Merge branch 'MDL-35147_22' of git://github.com/timhunt/moodle into MOODLE_22_STABLE
[moodle.git] / lib / ddl / sql_generator.php
blob203ccf3686fd3b950776d7650f9bb2bc7109053a
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
19 /**
20 * This class represent the base generator class where all the
21 * needed functions to generate proper SQL are defined.
23 * The rest of classes will inherit, by default, the same logic.
24 * Functions will be overridden as needed to generate correct SQL.
26 * @package core
27 * @subpackage ddl
28 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
29 * 2001-3001 Eloy Lafuente (stronk7) http://contiento.com
30 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33 defined('MOODLE_INTERNAL') || die();
35 /**
36 * Abstract sql generator class, base for all db specific implementations.
38 abstract class sql_generator {
40 /// Please, avoid editing this defaults in this base class!
41 /// It could change the behaviour of the rest of generators
42 /// that, by default, inherit this configuration.
43 /// To change any of them, do it in extended classes instead.
45 public $quote_string = '"'; // String used to quote names
47 public $statement_end = ';'; // String to be automatically added at the end of each statement
49 public $quote_all = false; // To decide if we want to quote all the names or only the reserved ones
51 public $integer_to_number = false; // To create all the integers as NUMBER(x) (also called DECIMAL, NUMERIC...)
52 public $float_to_number = false; // To create all the floats as NUMBER(x) (also called DECIMAL, NUMERIC...)
54 public $number_type = 'NUMERIC'; // Proper type for NUMBER(x) in this DB
56 public $unsigned_allowed = true; // To define in the generator must handle unsigned information
57 public $default_for_char = null; // To define the default to set for NOT NULLs CHARs without default (null=do nothing)
59 public $drop_default_value_required = false; //To specify if the generator must use some DEFAULT clause to drop defaults
60 public $drop_default_value = ''; //The DEFAULT clause required to drop defaults
62 public $default_after_null = true; //To decide if the default clause of each field must go after the null clause
64 public $specify_nulls = false; //To force the generator if NULL clauses must be specified. It shouldn't be necessary
65 //but some mssql drivers require them or everything is created as NOT NULL :-(
67 public $primary_key_name = null; //To force primary key names to one string (null=no force)
69 public $primary_keys = true; // Does the generator build primary keys
70 public $unique_keys = false; // Does the generator build unique keys
71 public $foreign_keys = false; // Does the generator build foreign keys
73 public $drop_primary_key = 'ALTER TABLE TABLENAME DROP CONSTRAINT KEYNAME'; // Template to drop PKs
74 // with automatic replace for TABLENAME and KEYNAME
76 public $drop_unique_key = 'ALTER TABLE TABLENAME DROP CONSTRAINT KEYNAME'; // Template to drop UKs
77 // with automatic replace for TABLENAME and KEYNAME
79 public $drop_foreign_key = 'ALTER TABLE TABLENAME DROP CONSTRAINT KEYNAME'; // Template to drop FKs
80 // with automatic replace for TABLENAME and KEYNAME
82 public $sequence_extra_code = true; //Does the generator need to add extra code to generate the sequence fields
83 public $sequence_name = 'auto_increment'; //Particular name for inline sequences in this generator
84 public $sequence_name_small = false; //Different name for small (4byte) sequences or false if same
85 public $sequence_only = false; //To avoid to output the rest of the field specs, leaving only the name and the sequence_name publiciable
87 public $add_table_comments = true; // Does the generator need to add code for table comments
89 public $add_after_clause = false; // Does the generator need to add the after clause for fields
91 public $prefix_on_names = true; //Does the generator need to prepend the prefix to all the key/index/sequence/trigger/check names
93 public $names_max_length = 30; //Max length for key/index/sequence/trigger/check names (keep 30 for all!)
95 public $concat_character = '||'; //Characters to be used as concatenation operator. If not defined
96 //MySQL CONCAT function will be used
98 public $rename_table_sql = 'ALTER TABLE OLDNAME RENAME TO NEWNAME'; //SQL sentence to rename one table, both
99 //OLDNAME and NEWNAME are dynamically replaced
101 public $drop_table_sql = 'DROP TABLE TABLENAME'; //SQL sentence to drop one table
102 //TABLENAME is dynamically replaced
104 public $alter_column_sql = 'ALTER TABLE TABLENAME ALTER COLUMN COLUMNSPECS'; //The SQL template to alter columns
106 public $alter_column_skip_default = false; //The generator will skip the default clause on alter columns
108 public $alter_column_skip_type = false; //The generator will skip the type clause on alter columns
110 public $alter_column_skip_notnull = false; //The generator will skip the null/notnull clause on alter columns
112 public $rename_column_sql = 'ALTER TABLE TABLENAME RENAME COLUMN OLDFIELDNAME TO NEWFIELDNAME';
113 ///TABLENAME, OLDFIELDNAME and NEWFIELDNAME are dyanmically replaced
115 public $drop_index_sql = 'DROP INDEX INDEXNAME'; //SQL sentence to drop one index
116 //TABLENAME, INDEXNAME are dynamically replaced
118 public $rename_index_sql = 'ALTER INDEX OLDINDEXNAME RENAME TO NEWINDEXNAME'; //SQL sentence to rename one index
119 //TABLENAME, OLDINDEXNAME, NEWINDEXNAME are dynamically replaced
121 public $rename_key_sql = 'ALTER TABLE TABLENAME CONSTRAINT OLDKEYNAME RENAME TO NEWKEYNAME'; //SQL sentence to rename one key
122 //TABLENAME, OLDKEYNAME, NEWKEYNAME are dynamically replaced
124 public $prefix; // Prefix to be used for all the DB objects
126 public $reserved_words; // List of reserved words (in order to quote them properly)
128 public $mdb;
130 protected $temptables; // Control existing temptables
133 * Creates new sql_generator
134 * @param object moodle_database instance
136 public function __construct($mdb, $temptables = null) {
137 $this->prefix = $mdb->get_prefix();
138 $this->reserved_words = $this->getReservedWords();
139 $this->mdb = $mdb; // this creates circular reference - the other link must be unset when closing db
140 $this->temptables = $temptables;
144 * Release all resources
146 public function dispose() {
147 $this->mdb = null;
151 * Given one string (or one array), ends it with statement_end
153 public function getEndedStatements($input) {
155 if (is_array($input)) {
156 foreach ($input as $key=>$content) {
157 $input[$key] = $this->getEndedStatements($content);
159 return $input;
160 } else {
161 $input = trim($input).$this->statement_end;
162 return $input;
167 * Given one xmldb_table, check if it exists in DB (true/false)
169 * @param mixed the table to be searched (string name or xmldb_table instance)
170 * @return boolean true/false
172 public function table_exists($table) {
173 if (is_string($table)) {
174 $tablename = $table;
175 } else {
176 /// Calculate the name of the table
177 $tablename = $table->getName();
180 /// get all tables in moodle database
181 $tables = $this->mdb->get_tables();
182 $exists = in_array($tablename, $tables);
184 return $exists;
188 * This function will return the SQL code needed to create db tables and statements
190 public function getCreateStructureSQL($xmldb_structure) {
191 $results = array();
193 if ($tables = $xmldb_structure->getTables()) {
194 foreach ($tables as $table) {
195 $results = array_merge($results, $this->getCreateTableSQL($table));
199 return $results;
203 * Given one xmldb_table, returns it's correct name, depending of all the parametrization
205 * @param xmldb_table table whose name we want
206 * @param boolean to specify if the name must be quoted (if reserved word, only!)
207 * @return string the correct name of the table
209 public function getTableName(xmldb_table $xmldb_table, $quoted=true) {
210 /// Get the name
211 $tablename = $this->prefix.$xmldb_table->getName();
213 /// Apply quotes optionally
214 if ($quoted) {
215 $tablename = $this->getEncQuoted($tablename);
218 return $tablename;
222 * Given one correct xmldb_table, returns the SQL statements
223 * to create it (inside one array)
225 public function getCreateTableSQL($xmldb_table) {
227 $results = array(); //Array where all the sentences will be stored
229 /// Table header
230 $table = 'CREATE TABLE ' . $this->getTableName($xmldb_table) . ' (';
232 if (!$xmldb_fields = $xmldb_table->getFields()) {
233 return $results;
236 $sequencefield = null;
238 /// Add the fields, separated by commas
239 foreach ($xmldb_fields as $xmldb_field) {
240 if ($xmldb_field->getSequence()) {
241 $sequencefield = $xmldb_field->getName();
243 $table .= "\n " . $this->getFieldSQL($xmldb_table, $xmldb_field);
244 $table .= ',';
246 /// Add the keys, separated by commas
247 if ($xmldb_keys = $xmldb_table->getKeys()) {
248 foreach ($xmldb_keys as $xmldb_key) {
249 if ($keytext = $this->getKeySQL($xmldb_table, $xmldb_key)) {
250 $table .= "\nCONSTRAINT " . $keytext . ',';
252 /// If the key is XMLDB_KEY_FOREIGN_UNIQUE, create it as UNIQUE too
253 if ($xmldb_key->getType() == XMLDB_KEY_FOREIGN_UNIQUE) {
254 ///Duplicate the key
255 $xmldb_key->setType(XMLDB_KEY_UNIQUE);
256 if ($keytext = $this->getKeySQL($xmldb_table, $xmldb_key)) {
257 $table .= "\nCONSTRAINT " . $keytext . ',';
260 /// make sure sequence field is unique
261 if ($sequencefield and $xmldb_key->getType() == XMLDB_KEY_PRIMARY) {
262 $field = reset($xmldb_key->getFields());
263 if ($sequencefield === $field) {
264 $sequencefield = null;
269 /// throw error if sequence field does not have unique key defined
270 if ($sequencefield) {
271 throw new ddl_exception('ddsequenceerror', $xmldb_table->getName());
274 /// Table footer, trim the latest comma
275 $table = trim($table,',');
276 $table .= "\n)";
278 /// Add the CREATE TABLE to results
279 $results[] = $table;
281 /// Add comments if specified and it exists
282 if ($this->add_table_comments && $xmldb_table->getComment()) {
283 $comment = $this->getCommentSQL($xmldb_table);
284 /// Add the COMMENT to results
285 $results = array_merge($results, $comment);
288 /// Add the indexes (each one, one statement)
289 if ($xmldb_indexes = $xmldb_table->getIndexes()) {
290 foreach ($xmldb_indexes as $xmldb_index) {
291 ///tables do not exist yet, which means indexed can not exist yet
292 if ($indextext = $this->getCreateIndexSQL($xmldb_table, $xmldb_index)) {
293 $results = array_merge($results, $indextext);
298 /// Also, add the indexes needed from keys, based on configuration (each one, one statement)
299 if ($xmldb_keys = $xmldb_table->getKeys()) {
300 foreach ($xmldb_keys as $xmldb_key) {
301 /// If we aren't creating the keys OR if the key is XMLDB_KEY_FOREIGN (not underlying index generated
302 /// automatically by the RDBMS) create the underlying (created by us) index (if doesn't exists)
303 if (!$this->getKeySQL($xmldb_table, $xmldb_key) || $xmldb_key->getType() == XMLDB_KEY_FOREIGN) {
304 /// Create the interim index
305 $index = new xmldb_index('anyname');
306 $index->setFields($xmldb_key->getFields());
307 ///tables do not exist yet, which means indexed can not exist yet
308 $createindex = false; //By default
309 switch ($xmldb_key->getType()) {
310 case XMLDB_KEY_UNIQUE:
311 case XMLDB_KEY_FOREIGN_UNIQUE:
312 $index->setUnique(true);
313 $createindex = true;
314 break;
315 case XMLDB_KEY_FOREIGN:
316 $index->setUnique(false);
317 $createindex = true;
318 break;
320 if ($createindex) {
321 if ($indextext = $this->getCreateIndexSQL($xmldb_table, $index)) {
322 /// Add the INDEX to the array
323 $results = array_merge($results, $indextext);
330 /// Add sequence extra code if needed
331 if ($this->sequence_extra_code) {
332 /// Iterate over fields looking for sequences
333 foreach ($xmldb_fields as $xmldb_field) {
334 if ($xmldb_field->getSequence()) {
335 /// returns an array of statements needed to create one sequence
336 $sequence_sentences = $this->getCreateSequenceSQL($xmldb_table, $xmldb_field);
337 /// Add the SEQUENCE to the array
338 $results = array_merge($results, $sequence_sentences);
343 return $results;
347 * Given one correct xmldb_index, returns the SQL statements
348 * needed to create it (in array)
350 public function getCreateIndexSQL($xmldb_table, $xmldb_index) {
351 if ($error = $xmldb_index->validateDefinition($xmldb_table)) {
352 throw new coding_exception($error);
355 $unique = '';
356 $suffix = 'ix';
357 if ($xmldb_index->getUnique()) {
358 $unique = ' UNIQUE';
359 $suffix = 'uix';
362 $index = 'CREATE' . $unique . ' INDEX ';
363 $index .= $this->getNameForObject($xmldb_table->getName(), implode(', ', $xmldb_index->getFields()), $suffix);
364 $index .= ' ON ' . $this->getTableName($xmldb_table);
365 $index .= ' (' . implode(', ', $this->getEncQuoted($xmldb_index->getFields())) . ')';
367 return array($index);
371 * Given one correct xmldb_field, returns the complete SQL line to create it
373 public function getFieldSQL($xmldb_table, $xmldb_field, $skip_type_clause = NULL, $skip_default_clause = NULL, $skip_notnull_clause = NULL, $specify_nulls_clause = NULL, $specify_field_name = true) {
374 if ($error = $xmldb_field->validateDefinition($xmldb_table)) {
375 throw new coding_exception($error);
378 $skip_type_clause = is_null($skip_type_clause) ? $this->alter_column_skip_type : $skip_type_clause;
379 $skip_default_clause = is_null($skip_default_clause) ? $this->alter_column_skip_default : $skip_default_clause;
380 $skip_notnull_clause = is_null($skip_notnull_clause) ? $this->alter_column_skip_notnull : $skip_notnull_clause;
381 $specify_nulls_clause = is_null($specify_nulls_clause) ? $this->specify_nulls : $specify_nulls_clause;
383 /// First of all, convert integers to numbers if defined
384 if ($this->integer_to_number) {
385 if ($xmldb_field->getType() == XMLDB_TYPE_INTEGER) {
386 $xmldb_field->setType(XMLDB_TYPE_NUMBER);
389 /// Same for floats
390 if ($this->float_to_number) {
391 if ($xmldb_field->getType() == XMLDB_TYPE_FLOAT) {
392 $xmldb_field->setType(XMLDB_TYPE_NUMBER);
396 $field = ''; // Let's accumulate the whole expression based on params and settings
397 /// The name
398 if ($specify_field_name) {
399 $field .= $this->getEncQuoted($xmldb_field->getName());
401 /// The type and length only if we don't want to skip it
402 if (!$skip_type_clause) {
403 /// The type and length
404 $field .= ' ' . $this->getTypeSQL($xmldb_field->getType(), $xmldb_field->getLength(), $xmldb_field->getDecimals());
406 /// The unsigned if supported
407 if ($this->unsigned_allowed && ($xmldb_field->getType() == XMLDB_TYPE_INTEGER ||
408 $xmldb_field->getType() == XMLDB_TYPE_NUMBER ||
409 $xmldb_field->getType() == XMLDB_TYPE_FLOAT)) {
410 if ($xmldb_field->getUnsigned()) {
411 $field .= ' unsigned';
414 /// Calculate the not null clause
415 $notnull = '';
416 /// Only if we don't want to skip it
417 if (!$skip_notnull_clause) {
418 if ($xmldb_field->getNotNull()) {
419 $notnull = ' NOT NULL';
420 } else {
421 if ($specify_nulls_clause) {
422 $notnull = ' NULL';
426 /// Calculate the default clause
427 $default_clause = '';
428 if (!$skip_default_clause) { //Only if we don't want to skip it
429 $default_clause = $this->getDefaultClause($xmldb_field);
431 /// Based on default_after_null, set both clauses properly
432 if ($this->default_after_null) {
433 $field .= $notnull . $default_clause;
434 } else {
435 $field .= $default_clause . $notnull;
437 /// The sequence
438 if ($xmldb_field->getSequence()) {
439 if($xmldb_field->getLength()<=9 && $this->sequence_name_small) {
440 $sequencename=$this->sequence_name_small;
441 } else {
442 $sequencename=$this->sequence_name;
444 $field .= ' ' . $sequencename;
445 if ($this->sequence_only) {
446 /// We only want the field name and sequence name to be printed
447 /// so, calculate it and return
448 $sql = $this->getEncQuoted($xmldb_field->getName()) . ' ' . $sequencename;
449 return $sql;
452 return $field;
456 * Given one correct xmldb_key, returns its specs
458 public function getKeySQL($xmldb_table, $xmldb_key) {
460 $key = '';
462 switch ($xmldb_key->getType()) {
463 case XMLDB_KEY_PRIMARY:
464 if ($this->primary_keys) {
465 if ($this->primary_key_name !== null) {
466 $key = $this->getEncQuoted($this->primary_key_name);
467 } else {
468 $key = $this->getNameForObject($xmldb_table->getName(), implode(', ', $xmldb_key->getFields()), 'pk');
470 $key .= ' PRIMARY KEY (' . implode(', ', $this->getEncQuoted($xmldb_key->getFields())) . ')';
472 break;
473 case XMLDB_KEY_UNIQUE:
474 if ($this->unique_keys) {
475 $key = $this->getNameForObject($xmldb_table->getName(), implode(', ', $xmldb_key->getFields()), 'uk');
476 $key .= ' UNIQUE (' . implode(', ', $this->getEncQuoted($xmldb_key->getFields())) . ')';
478 break;
479 case XMLDB_KEY_FOREIGN:
480 case XMLDB_KEY_FOREIGN_UNIQUE:
481 if ($this->foreign_keys) {
482 $key = $this->getNameForObject($xmldb_table->getName(), implode(', ', $xmldb_key->getFields()), 'fk');
483 $key .= ' FOREIGN KEY (' . implode(', ', $this->getEncQuoted($xmldb_key->getFields())) . ')';
484 $key .= ' REFERENCES ' . $this->getEncQuoted($this->prefix . $xmldb_key->getRefTable());
485 $key .= ' (' . implode(', ', $this->getEncQuoted($xmldb_key->getRefFields())) . ')';
487 break;
490 return $key;
494 * Give one xmldb_field, returns the correct "default value" for the current configuration
496 public function getDefaultValue($xmldb_field) {
498 $default = null;
500 if ($xmldb_field->getDefault() !== NULL) {
501 if ($xmldb_field->getType() == XMLDB_TYPE_CHAR ||
502 $xmldb_field->getType() == XMLDB_TYPE_TEXT) {
503 if ($xmldb_field->getDefault() === '') { // If passing empty default, use the $default_for_char one instead
504 $default = "'" . $this->default_for_char . "'";
505 } else {
506 $default = "'" . $this->addslashes($xmldb_field->getDefault()) . "'";
508 } else {
509 $default = $xmldb_field->getDefault();
511 } else {
512 /// We force default '' for not null char columns without proper default
513 /// some day this should be out!
514 if ($this->default_for_char !== NULL &&
515 $xmldb_field->getType() == XMLDB_TYPE_CHAR &&
516 $xmldb_field->getNotNull()) {
517 $default = "'" . $this->default_for_char . "'";
518 } else {
519 /// If the DB requires to explicity define some clause to drop one default, do it here
520 /// never applying defaults to TEXT and BINARY fields
521 if ($this->drop_default_value_required &&
522 $xmldb_field->getType() != XMLDB_TYPE_TEXT &&
523 $xmldb_field->getType() != XMLDB_TYPE_BINARY && !$xmldb_field->getNotNull()) {
524 $default = $this->drop_default_value;
528 return $default;
532 * Given one xmldb_field, returns the correct "default clause" for the current configuration
534 public function getDefaultClause($xmldb_field) {
536 $defaultvalue = $this->getDefaultValue ($xmldb_field);
538 if ($defaultvalue !== null) {
539 return ' DEFAULT ' . $defaultvalue;
540 } else {
541 return null;
546 * Given one correct xmldb_table and the new name, returns the SQL statements
547 * to rename it (inside one array)
549 public function getRenameTableSQL($xmldb_table, $newname) {
551 $results = array(); //Array where all the sentences will be stored
553 $newt = new xmldb_table($newname); //Temporal table for name calculations
555 $rename = str_replace('OLDNAME', $this->getTableName($xmldb_table), $this->rename_table_sql);
556 $rename = str_replace('NEWNAME', $this->getTableName($newt), $rename);
558 $results[] = $rename;
560 /// Call to getRenameTableExtraSQL() override if needed
561 $extra_sentences = $this->getRenameTableExtraSQL($xmldb_table, $newname);
562 $results = array_merge($results, $extra_sentences);
564 return $results;
568 * Given one correct xmldb_table and the new name, returns the SQL statements
569 * to drop it (inside one array)
571 public function getDropTableSQL($xmldb_table) {
573 $results = array(); //Array where all the sentences will be stored
575 $drop = str_replace('TABLENAME', $this->getTableName($xmldb_table), $this->drop_table_sql);
577 $results[] = $drop;
579 /// call to getDropTableExtraSQL(), override if needed
580 $extra_sentences = $this->getDropTableExtraSQL($xmldb_table);
581 $results = array_merge($results, $extra_sentences);
583 return $results;
587 * Given one xmldb_table and one xmldb_field, return the SQL statements needed to add the field to the table
589 public function getAddFieldSQL($xmldb_table, $xmldb_field, $skip_type_clause = NULL, $skip_default_clause = NULL, $skip_notnull_clause = NULL) {
591 $skip_type_clause = is_null($skip_type_clause) ? $this->alter_column_skip_type : $skip_type_clause;
592 $skip_default_clause = is_null($skip_default_clause) ? $this->alter_column_skip_default : $skip_default_clause;
593 $skip_notnull_clause = is_null($skip_notnull_clause) ? $this->alter_column_skip_notnull : $skip_notnull_clause;
595 $results = array();
597 /// Get the quoted name of the table and field
598 $tablename = $this->getTableName($xmldb_table);
600 /// Build the standard alter table add
601 $sql = $this->getFieldSQL($xmldb_table, $xmldb_field, $skip_type_clause,
602 $skip_default_clause,
603 $skip_notnull_clause);
604 $altertable = 'ALTER TABLE ' . $tablename . ' ADD ' . $sql;
605 /// Add the after clause if necesary
606 if ($this->add_after_clause && $xmldb_field->getPrevious()) {
607 $altertable .= ' AFTER ' . $this->getEncQuoted($xmldb_field->getPrevious());
609 $results[] = $altertable;
611 return $results;
615 * Given one xmldb_table and one xmldb_field, return the SQL statements needed to drop the field from the table
617 public function getDropFieldSQL($xmldb_table, $xmldb_field) {
619 $results = array();
621 /// Get the quoted name of the table and field
622 $tablename = $this->getTableName($xmldb_table);
623 $fieldname = $this->getEncQuoted($xmldb_field->getName());
625 /// Build the standard alter table drop
626 $results[] = 'ALTER TABLE ' . $tablename . ' DROP COLUMN ' . $fieldname;
628 return $results;
632 * Given one xmldb_table and one xmldb_field, return the SQL statements needed to alter the field in the table
634 public function getAlterFieldSQL($xmldb_table, $xmldb_field, $skip_type_clause = NULL, $skip_default_clause = NULL, $skip_notnull_clause = NULL) {
636 $skip_type_clause = is_null($skip_type_clause) ? $this->alter_column_skip_type : $skip_type_clause;
637 $skip_default_clause = is_null($skip_default_clause) ? $this->alter_column_skip_default : $skip_default_clause;
638 $skip_notnull_clause = is_null($skip_notnull_clause) ? $this->alter_column_skip_notnull : $skip_notnull_clause;
640 $results = array();
642 /// Get the quoted name of the table and field
643 $tablename = $this->getTableName($xmldb_table);
644 $fieldname = $this->getEncQuoted($xmldb_field->getName());
646 /// Build de alter sentence using the alter_column_sql template
647 $alter = str_replace('TABLENAME', $this->getTableName($xmldb_table), $this->alter_column_sql);
648 $colspec = $this->getFieldSQL($xmldb_table, $xmldb_field, $skip_type_clause,
649 $skip_default_clause,
650 $skip_notnull_clause,
651 true);
652 $alter = str_replace('COLUMNSPECS', $colspec, $alter);
654 /// Add the after clause if necesary
655 if ($this->add_after_clause && $xmldb_field->getPrevious()) {
656 $alter .= ' after ' . $this->getEncQuoted($xmldb_field->getPrevious());
659 /// Build the standard alter table modify
660 $results[] = $alter;
662 return $results;
666 * Given one xmldb_table and one xmldb_field, return the SQL statements needed to modify the default of the field in the table
668 public function getModifyDefaultSQL($xmldb_table, $xmldb_field) {
670 $results = array();
672 /// Get the quoted name of the table and field
673 $tablename = $this->getTableName($xmldb_table);
674 $fieldname = $this->getEncQuoted($xmldb_field->getName());
676 /// Decide if we are going to create/modify or to drop the default
677 if ($xmldb_field->getDefault() === null) {
678 $results = $this->getDropDefaultSQL($xmldb_table, $xmldb_field); //Drop
679 } else {
680 $results = $this->getCreateDefaultSQL($xmldb_table, $xmldb_field); //Create/modify
683 return $results;
687 * Given one correct xmldb_field and the new name, returns the SQL statements
688 * to rename it (inside one array)
690 public function getRenameFieldSQL($xmldb_table, $xmldb_field, $newname) {
692 $results = array(); //Array where all the sentences will be stored
694 /// Although this is checked in database_manager::rename_field() - double check
695 /// that we aren't trying to rename one "id" field. Although it could be
696 /// implemented (if adding the necessary code to rename sequences, defaults,
697 /// triggers... and so on under each getRenameFieldExtraSQL() function, it's
698 /// better to forbid it, mainly because this field is the default PK and
699 /// in the future, a lot of FKs can be pointing here. So, this field, more
700 /// or less, must be considered immutable!
701 if ($xmldb_field->getName() == 'id') {
702 return array();
705 $rename = str_replace('TABLENAME', $this->getTableName($xmldb_table), $this->rename_column_sql);
706 $rename = str_replace('OLDFIELDNAME', $this->getEncQuoted($xmldb_field->getName()), $rename);
707 $rename = str_replace('NEWFIELDNAME', $this->getEncQuoted($newname), $rename);
709 $results[] = $rename;
711 /// Call to getRenameFieldExtraSQL(), override if needed
712 $extra_sentences = $this->getRenameFieldExtraSQL($xmldb_table, $xmldb_field, $newname);
713 $results = array_merge($results, $extra_sentences);
715 return $results;
719 * Given one xmldb_table and one xmldb_key, return the SQL statements needed to add the key to the table
720 * note that undelying indexes will be added as parametrised by $xxxx_keys and $xxxx_index parameters
722 public function getAddKeySQL($xmldb_table, $xmldb_key) {
724 $results = array();
726 /// Just use the CreateKeySQL function
727 if ($keyclause = $this->getKeySQL($xmldb_table, $xmldb_key)) {
728 $key = 'ALTER TABLE ' . $this->getTableName($xmldb_table) .
729 ' ADD CONSTRAINT ' . $keyclause;
730 $results[] = $key;
733 /// If we aren't creating the keys OR if the key is XMLDB_KEY_FOREIGN (not underlying index generated
734 /// automatically by the RDBMS) create the underlying (created by us) index (if doesn't exists)
735 if (!$keyclause || $xmldb_key->getType() == XMLDB_KEY_FOREIGN) {
736 /// Only if they don't exist
737 if ($xmldb_key->getType() == XMLDB_KEY_FOREIGN) { ///Calculate type of index based on type ok key
738 $indextype = XMLDB_INDEX_NOTUNIQUE;
739 } else {
740 $indextype = XMLDB_INDEX_UNIQUE;
742 $xmldb_index = new xmldb_index('anyname', $indextype, $xmldb_key->getFields());
743 if (!$this->mdb->get_manager()->index_exists($xmldb_table, $xmldb_index)) {
744 $results = array_merge($results, $this->getAddIndexSQL($xmldb_table, $xmldb_index));
748 /// If the key is XMLDB_KEY_FOREIGN_UNIQUE, create it as UNIQUE too
749 if ($xmldb_key->getType() == XMLDB_KEY_FOREIGN_UNIQUE && $this->unique_keys) {
750 ///Duplicate the key
751 $xmldb_key->setType(XMLDB_KEY_UNIQUE);
752 $results = array_merge($results, $this->getAddKeySQL($xmldb_table, $xmldb_key));
755 /// Return results
756 return $results;
760 * Given one xmldb_table and one xmldb_index, return the SQL statements needed to drop the index from the table
762 public function getDropKeySQL($xmldb_table, $xmldb_key) {
764 $results = array();
766 /// Get the key name (note that this doesn't introspect DB, so could cause some problems sometimes!)
767 /// TODO: We'll need to overwrite the whole getDropKeySQL() method inside each DB to do the proper queries
768 /// against the dictionary or require ADOdb to support it or change the find_key_name() method to
769 /// perform DB introspection directly. But, for now, as we aren't going to enable referential integrity
770 /// it won't be a problem at all
771 $dbkeyname = $this->mdb->get_manager()->find_key_name($xmldb_table, $xmldb_key);
773 /// Only if such type of key generation is enabled
774 $dropkey = false;
775 switch ($xmldb_key->getType()) {
776 case XMLDB_KEY_PRIMARY:
777 if ($this->primary_keys) {
778 $template = $this->drop_primary_key;
779 $dropkey = true;
781 break;
782 case XMLDB_KEY_UNIQUE:
783 if ($this->unique_keys) {
784 $template = $this->drop_unique_key;
785 $dropkey = true;
787 break;
788 case XMLDB_KEY_FOREIGN_UNIQUE:
789 case XMLDB_KEY_FOREIGN:
790 if ($this->foreign_keys) {
791 $template = $this->drop_foreign_key;
792 $dropkey = true;
794 break;
796 /// If we have decided to drop the key, let's do it
797 if ($dropkey) {
798 /// Replace TABLENAME, CONSTRAINTTYPE and KEYNAME as needed
799 $dropsql = str_replace('TABLENAME', $this->getTableName($xmldb_table), $template);
800 $dropsql = str_replace('KEYNAME', $dbkeyname, $dropsql);
802 $results[] = $dropsql;
805 /// If we aren't dropping the keys OR if the key is XMLDB_KEY_FOREIGN (not underlying index generated
806 /// automatically by the RDBMS) drop the underlying (created by us) index (if exists)
807 if (!$dropkey || $xmldb_key->getType() == XMLDB_KEY_FOREIGN) {
808 /// Only if they exist
809 $xmldb_index = new xmldb_index('anyname', XMLDB_INDEX_UNIQUE, $xmldb_key->getFields());
810 if ($this->mdb->get_manager()->index_exists($xmldb_table, $xmldb_index)) {
811 $results = array_merge($results, $this->getDropIndexSQL($xmldb_table, $xmldb_index));
815 /// If the key is XMLDB_KEY_FOREIGN_UNIQUE, drop the UNIQUE too
816 if ($xmldb_key->getType() == XMLDB_KEY_FOREIGN_UNIQUE && $this->unique_keys) {
817 ///Duplicate the key
818 $xmldb_key->setType(XMLDB_KEY_UNIQUE);
819 $results = array_merge($results, $this->getDropKeySQL($xmldb_table, $xmldb_key));
822 /// Return results
823 return $results;
827 * Given one xmldb_table and one xmldb_key, return the SQL statements needed to rename the key in the table
828 * Experimental! Shouldn't be used at all!
831 public function getRenameKeySQL($xmldb_table, $xmldb_key, $newname) {
833 $results = array();
835 /// Get the real key name
836 $dbkeyname = $this->mdb->get_manager()->find_key_name($xmldb_table, $xmldb_key);
838 /// Check we are really generating this type of keys
839 if (($xmldb_key->getType() == XMLDB_KEY_PRIMARY && !$this->primary_keys) ||
840 ($xmldb_key->getType() == XMLDB_KEY_UNIQUE && !$this->unique_keys) ||
841 ($xmldb_key->getType() == XMLDB_KEY_FOREIGN && !$this->foreign_keys) ||
842 ($xmldb_key->getType() == XMLDB_KEY_FOREIGN_UNIQUE && !$this->unique_keys && !$this->foreign_keys)) {
843 /// We aren't generating this type of keys, delegate to child indexes
844 $xmldb_index = new xmldb_index($xmldb_key->getName());
845 $xmldb_index->setFields($xmldb_key->getFields());
846 return $this->getRenameIndexSQL($xmldb_table, $xmldb_index, $newname);
849 /// Arrived here so we are working with keys, lets rename them
850 /// Replace TABLENAME and KEYNAME as needed
851 $renamesql = str_replace('TABLENAME', $this->getTableName($xmldb_table), $this->rename_key_sql);
852 $renamesql = str_replace('OLDKEYNAME', $dbkeyname, $renamesql);
853 $renamesql = str_replace('NEWKEYNAME', $newname, $renamesql);
855 /// Some DB doesn't support key renaming so this can be empty
856 if ($renamesql) {
857 $results[] = $renamesql;
860 return $results;
864 * Given one xmldb_table and one xmldb_index, return the SQL statements needed to add the index to the table
866 public function getAddIndexSQL($xmldb_table, $xmldb_index) {
868 /// Just use the CreateIndexSQL function
869 return $this->getCreateIndexSQL($xmldb_table, $xmldb_index);
873 * Given one xmldb_table and one xmldb_index, return the SQL statements needed to drop the index from the table
875 public function getDropIndexSQL($xmldb_table, $xmldb_index) {
877 $results = array();
879 /// Get the real index name
880 $dbindexname = $this->mdb->get_manager()->find_index_name($xmldb_table, $xmldb_index);
882 /// Replace TABLENAME and INDEXNAME as needed
883 $dropsql = str_replace('TABLENAME', $this->getTableName($xmldb_table), $this->drop_index_sql);
884 $dropsql = str_replace('INDEXNAME', $this->getEncQuoted($dbindexname), $dropsql);
886 $results[] = $dropsql;
888 return $results;
892 * Given one xmldb_table and one xmldb_index, return the SQL statements needed to rename the index in the table
893 * Experimental! Shouldn't be used at all!
895 function getRenameIndexSQL($xmldb_table, $xmldb_index, $newname) {
896 /// Some DB doesn't support index renaming (MySQL) so this can be empty
897 if (empty($this->rename_index_sql)) {
898 return array();
901 /// Get the real index name
902 $dbindexname = $this->mdb->get_manager()->find_index_name($xmldb_table, $xmldb_index);
903 /// Replace TABLENAME and INDEXNAME as needed
904 $renamesql = str_replace('TABLENAME', $this->getTableName($xmldb_table), $this->rename_index_sql);
905 $renamesql = str_replace('OLDINDEXNAME', $this->getEncQuoted($dbindexname), $renamesql);
906 $renamesql = str_replace('NEWINDEXNAME', $this->getEncQuoted($newname), $renamesql);
908 return array($renamesql);
912 * Given three strings (table name, list of fields (comma separated) and suffix),
913 * create the proper object name quoting it if necessary.
915 * IMPORTANT: This function must be used to CALCULATE NAMES of objects TO BE CREATED,
916 * NEVER TO GUESS NAMES of EXISTING objects!!!
918 public function getNameForObject($tablename, $fields, $suffix='') {
920 $name = '';
922 /// Implement one basic cache to avoid object name duplication
923 /// along all the request life, but never to return cached results
924 /// We need this because sql statements are created before executing
925 /// them, hence names doesn't exist "physically" yet in DB, so we need
926 /// to known which ones have been used
927 static $used_names = array();
929 /// Use standard naming. See http://docs.moodle.org/en/XMLDB_key_and_index_naming
930 $tablearr = explode ('_', $tablename);
931 foreach ($tablearr as $table) {
932 $name .= substr(trim($table),0,4);
934 $name .= '_';
935 $fieldsarr = explode (',', $fields);
936 foreach ($fieldsarr as $field) {
937 $name .= substr(trim($field),0,3);
939 /// Prepend the prefix
940 $name = $this->prefix . $name;
942 $name = substr(trim($name), 0, $this->names_max_length - 1 - strlen($suffix)); //Max names_max_length
944 /// Add the suffix
945 $namewithsuffix = $name;
946 if ($suffix) {
947 $namewithsuffix = $namewithsuffix . '_' . $suffix;
950 /// If the calculated name is in the cache, or if we detect it by introspecting the DB let's modify if
951 if (in_array($namewithsuffix, $used_names) || $this->isNameInUse($namewithsuffix, $suffix, $tablename)) {
952 $counter = 2;
953 /// If have free space, we add 2
954 if (strlen($namewithsuffix) < $this->names_max_length) {
955 $newname = $name . $counter;
956 /// Else replace the last char by 2
957 } else {
958 $newname = substr($name, 0, strlen($name)-1) . $counter;
960 $newnamewithsuffix = $newname;
961 if ($suffix) {
962 $newnamewithsuffix = $newnamewithsuffix . '_' . $suffix;
964 /// Now iterate until not used name is found, incrementing the counter
965 while (in_array($newnamewithsuffix, $used_names) || $this->isNameInUse($newnamewithsuffix, $suffix, $tablename)) {
966 $counter++;
967 $newname = substr($name, 0, strlen($newname)-1) . $counter;
968 $newnamewithsuffix = $newname;
969 if ($suffix) {
970 $newnamewithsuffix = $newnamewithsuffix . '_' . $suffix;
973 $namewithsuffix = $newnamewithsuffix;
976 /// Add the name to the cache
977 $used_names[] = $namewithsuffix;
979 /// Quote it if necessary (reserved words)
980 $namewithsuffix = $this->getEncQuoted($namewithsuffix);
982 return $namewithsuffix;
986 * Given any string (or one array), enclose it by the proper quotes
987 * if it's a reserved word
989 public function getEncQuoted($input) {
991 if (is_array($input)) {
992 foreach ($input as $key=>$content) {
993 $input[$key] = $this->getEncQuoted($content);
995 return $input;
996 } else {
997 /// Always lowercase
998 $input = strtolower($input);
999 /// if reserved or quote_all or has hyphens, quote it
1000 if ($this->quote_all || in_array($input, $this->reserved_words) || strpos($input, '-') !== false) {
1001 $input = $this->quote_string . $input . $this->quote_string;
1003 return $input;
1008 * Given one XMLDB Statement, build the needed SQL insert sentences to execute it
1010 function getExecuteInsertSQL($statement) {
1012 $results = array(); //Array where all the sentences will be stored
1014 if ($sentences = $statement->getSentences()) {
1015 foreach ($sentences as $sentence) {
1016 /// Get the list of fields
1017 $fields = $statement->getFieldsFromInsertSentence($sentence);
1018 /// Get the values of fields
1019 $values = $statement->getValuesFromInsertSentence($sentence);
1020 /// Look if we have some CONCAT value and transform it dynamically
1021 foreach($values as $key => $value) {
1022 /// Trim single quotes
1023 $value = trim($value,"'");
1024 if (stristr($value, 'CONCAT') !== false){
1025 /// Look for data between parenthesis
1026 preg_match("/CONCAT\s*\((.*)\)$/is", trim($value), $matches);
1027 if (isset($matches[1])) {
1028 $part = $matches[1];
1029 /// Convert the comma separated string to an array
1030 $arr = xmldb_object::comma2array($part);
1031 if ($arr) {
1032 $value = $this->getConcatSQL($arr);
1036 /// Values to be sent to DB must be properly escaped
1037 $value = $this->addslashes($value);
1038 /// Back trimmed quotes
1039 $value = "'" . $value . "'";
1040 /// Back to the array
1041 $values[$key] = $value;
1044 /// Iterate over fields, escaping them if necessary
1045 foreach($fields as $key => $field) {
1046 $fields[$key] = $this->getEncQuoted($field);
1048 /// Build the final SQL sentence and add it to the array of results
1049 $sql = 'INSERT INTO ' . $this->getEncQuoted($this->prefix . $statement->getTable()) .
1050 '(' . implode(', ', $fields) . ') ' .
1051 'VALUES (' . implode(', ', $values) . ')';
1052 $results[] = $sql;
1056 return $results;
1060 * Given one array of elements, build de proper CONCAT expression, based
1061 * in the $concat_character setting. If such setting is empty, then
1062 * MySQL's CONCAT function will be used instead
1064 public function getConcatSQL($elements) {
1066 /// Replace double quoted elements by single quotes
1067 foreach($elements as $key => $element) {
1068 $element = trim($element);
1069 if (substr($element, 0, 1) == '"' &&
1070 substr($element, -1, 1) == '"') {
1071 $elements[$key] = "'" . trim($element, '"') . "'";
1075 /// Now call the standard $DB->sql_concat() DML function
1076 return call_user_func_array(array($this->mdb, 'sql_concat'), $elements);
1080 * Returns the name (string) of the sequence used in the table for the autonumeric pk
1081 * Only some DB have this implemented
1083 public function getSequenceFromDB($xmldb_table) {
1084 return false;
1088 * Given one object name and it's type (pk, uk, fk, ck, ix, uix, seq, trg)
1089 * return if such name is currently in use (true) or no (false)
1090 * (MySQL requires the whole xmldb_table object to be specified, so we add it always)
1091 * (invoked from getNameForObject()
1092 * Only some DB have this implemented
1094 public function isNameInUse($object_name, $type, $table_name) {
1095 return false; //For generators not implementing introspection,
1096 //we always return with the name being free to be used
1100 /// ALL THESE FUNCTION MUST BE CUSTOMISED BY ALL THE XMLDGenerator classes
1103 * Reset a sequence to the id field of a table.
1104 * @param string $table name of table
1105 * @return success
1107 public abstract function getResetSequenceSQL($tablename);
1110 * Given one correct xmldb_table, returns the SQL statements
1111 * to create temporary table (inside one array)
1113 abstract public function getCreateTempTableSQL($xmldb_table);
1116 * Given one correct xmldb_table and the new name, returns the SQL statements
1117 * to drop it (inside one array)
1119 abstract public function getDropTempTableSQL($xmldb_table);
1122 * Given one XMLDB Type, length and decimals, returns the DB proper SQL type
1124 public abstract function getTypeSQL($xmldb_type, $xmldb_length=null, $xmldb_decimals=null);
1127 * Returns the code (array of statements) needed to execute extra statements on field rename
1129 public function getRenameFieldExtraSQL($xmldb_table, $xmldb_field) {
1130 return array();
1134 * Returns the code (array of statements) needed
1135 * to create one sequence for the xmldb_table and xmldb_field passes
1137 public function getCreateSequenceSQL($xmldb_table, $xmldb_field) {
1138 return array();
1142 * Returns the code (array of statements) needed to add one comment to the table
1144 public abstract function getCommentSQL($xmldb_table);
1147 * Returns the code (array of statements) needed to execute extra statements on table rename
1149 public function getRenameTableExtraSQL($xmldb_table, $newname) {
1150 return array();
1154 * Returns the code (array of statements) needed to execute extra statements on table drop
1156 public function getDropTableExtraSQL($xmldb_table) {
1157 return array();
1161 * Given one xmldb_table and one xmldb_field, return the SQL statements needed to drop its enum
1162 * (usually invoked from getModifyEnumSQL()
1164 * TODO: Moodle 2.1 - Drop getDropEnumSQL()
1166 public abstract function getDropEnumSQL($xmldb_table, $xmldb_field);
1169 * Given one xmldb_table and one xmldb_field, return the SQL statements needed to drop its default
1170 * (usually invoked from getModifyDefaultSQL()
1172 * TODO: Moodle 2.1 - Drop getDropDefaultSQL()
1174 public abstract function getDropDefaultSQL($xmldb_table, $xmldb_field);
1177 * Given one xmldb_table and one optional xmldb_field, return one array with all the check
1178 * constrainst found for that table (or field). Must exist for each DB supported.
1179 * (usually invoked from find_check_constraint_name)
1181 * TODO: Moodle 2.1 - Drop getCheckConstraintsFromDB
1183 public abstract function getCheckConstraintsFromDB($xmldb_table, $xmldb_field=null);
1186 * Given one xmldb_table and one xmldb_field, return the SQL statements needed to add its default
1187 * (usually invoked from getModifyDefaultSQL()
1189 public abstract function getCreateDefaultSQL($xmldb_table, $xmldb_field);
1192 * Returns an array of reserved words (lowercase) for this DB
1193 * You MUST provide the real list for each DB inside every XMLDB class
1194 * @return array of reserved words
1196 public static function getReservedWords() {
1197 throw new coding_exception('getReservedWords() method needs to be overridden in each subclass of sql_generator');
1201 * Returns all reserved works in supported databases.
1202 * Reserved words should be lowercase.
1203 * @return array ('word'=>array(databases))
1205 public static function getAllReservedWords() {
1206 global $CFG;
1208 $generators = array('mysql', 'postgres', 'oracle', 'mssql');
1209 $reserved_words = array();
1211 foreach($generators as $generator) {
1212 $class = $generator . '_sql_generator';
1213 require_once("$CFG->libdir/ddl/$class.php");
1214 foreach (call_user_func(array($class, 'getReservedWords')) as $word) {
1215 $reserved_words[$word][] = $generator;
1218 ksort($reserved_words);
1219 return $reserved_words;
1222 public function addslashes($s) {
1223 // do not use php addslashes() because it depends on PHP quote settings!
1224 $s = str_replace('\\','\\\\',$s);
1225 $s = str_replace("\0","\\\0", $s);
1226 $s = str_replace("'", "\\'", $s);
1227 return $s;