2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * This class represent one XMLDB table
21 * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
22 * 2001-3001 Eloy Lafuente (stronk7) http://contiento.com
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 defined('MOODLE_INTERNAL') ||
die();
29 class xmldb_table
extends xmldb_object
{
31 /** @var xmldb_field[] table columns */
34 /** @var xmldb_key[] keys */
37 /** @var xmldb_index[] indexes */
40 /** @var int max length of table name prefixes */
41 const PREFIX_MAX_LENGTH
= 10;
45 * - PostgreSQL has a limit of 63 ascii chars (bytes) for table names. Others have greater limits.
46 * Up to PREFIX_MAX_LENGTH ascii chars (bytes) are reserved for table prefixes.
48 * @var int max length of table names (without prefix).
50 const NAME_MAX_LENGTH
= 63 - self
::PREFIX_MAX_LENGTH
;
53 * Creates one new xmldb_table
56 public function __construct($name) {
57 parent
::__construct($name);
58 $this->fields
= array();
59 $this->keys
= array();
60 $this->indexes
= array();
64 * Add one field to the table, allowing to specify the desired order
65 * If it's not specified, then the field is added at the end
66 * @param xmldb_field $field
67 * @param xmldb_object $after
70 public function addField($field, $after=null) {
72 // Detect duplicates first
73 if ($this->getField($field->getName())) {
74 throw new coding_exception('Duplicate field '.$field->getName().' specified in table '.$this->getName());
77 // Calculate the previous and next fields
82 $allfields = $this->getFields();
83 if (!empty($allfields)) {
85 $prevfield = $allfields[key($allfields)];
88 $prevfield = $this->getField($after);
90 if ($prevfield && $prevfield->getNext()) {
91 $nextfield = $this->getField($prevfield->getNext());
94 // Set current field previous and next attributes
96 $field->setPrevious($prevfield->getName());
97 $prevfield->setNext($field->getName());
100 $field->setNext($nextfield->getName());
101 $nextfield->setPrevious($field->getName());
103 // Some more attributes
104 $field->setLoaded(true);
105 $field->setChanged(true);
107 $this->fields
[] = $field;
109 $this->orderFields($this->fields
);
110 // Recalculate the hash
111 $this->calculateHash(true);
112 // We have one new field, so the table has changed
113 $this->setChanged(true);
119 * Add one key to the table, allowing to specify the desired order
120 * If it's not specified, then the key is added at the end
121 * @param xmldb_key $key
122 * @param xmldb_object $after
124 public function addKey($key, $after=null) {
126 // Detect duplicates first
127 if ($this->getKey($key->getName())) {
128 throw new coding_exception('Duplicate key '.$key->getName().' specified in table '.$this->getName());
131 // Make sure there are no indexes with the key column specs because they would collide.
132 $newfields = $key->getFields();
133 $allindexes = $this->getIndexes();
134 foreach ($allindexes as $index) {
135 $fields = $index->getFields();
136 if ($fields === $newfields) {
137 throw new coding_exception('Index '.$index->getName().' collides with key'.$key->getName().' specified in table '.$this->getName());
141 // Calculate the previous and next keys
146 $allkeys = $this->getKeys();
147 if (!empty($allkeys)) {
149 $prevkey = $allkeys[key($allkeys)];
152 $prevkey = $this->getKey($after);
154 if ($prevkey && $prevkey->getNext()) {
155 $nextkey = $this->getKey($prevkey->getNext());
158 // Set current key previous and next attributes
160 $key->setPrevious($prevkey->getName());
161 $prevkey->setNext($key->getName());
164 $key->setNext($nextkey->getName());
165 $nextkey->setPrevious($key->getName());
167 // Some more attributes
168 $key->setLoaded(true);
169 $key->setChanged(true);
171 $this->keys
[] = $key;
173 $this->orderKeys($this->keys
);
174 // Recalculate the hash
175 $this->calculateHash(true);
176 // We have one new field, so the table has changed
177 $this->setChanged(true);
181 * Add one index to the table, allowing to specify the desired order
182 * If it's not specified, then the index is added at the end
183 * @param xmldb_index $index
184 * @param xmldb_object $after
186 public function addIndex($index, $after=null) {
188 // Detect duplicates first
189 if ($this->getIndex($index->getName())) {
190 throw new coding_exception('Duplicate index '.$index->getName().' specified in table '.$this->getName());
193 // Make sure there are no keys with the index column specs because they would collide.
194 $newfields = $index->getFields();
195 $allkeys = $this->getKeys();
196 foreach ($allkeys as $key) {
197 $fields = $key->getFields();
198 if ($fields === $newfields) {
199 throw new coding_exception('Key '.$key->getName().' collides with index'.$index->getName().' specified in table '.$this->getName());
203 // Calculate the previous and next indexes
208 $allindexes = $this->getIndexes();
209 if (!empty($allindexes)) {
211 $previndex = $allindexes[key($allindexes)];
214 $previndex = $this->getIndex($after);
216 if ($previndex && $previndex->getNext()) {
217 $nextindex = $this->getIndex($previndex->getNext());
220 // Set current index previous and next attributes
222 $index->setPrevious($previndex->getName());
223 $previndex->setNext($index->getName());
226 $index->setNext($nextindex->getName());
227 $nextindex->setPrevious($index->getName());
230 // Some more attributes
231 $index->setLoaded(true);
232 $index->setChanged(true);
234 $this->indexes
[] = $index;
235 // Reorder the indexes
236 $this->orderIndexes($this->indexes
);
237 // Recalculate the hash
238 $this->calculateHash(true);
239 // We have one new index, so the table has changed
240 $this->setChanged(true);
244 * This function will return the array of fields in the table
245 * @return xmldb_field[]
247 public function getFields() {
248 return $this->fields
;
252 * This function will return the array of keys in the table
253 * @return xmldb_key[]
255 public function getKeys() {
260 * This function will return the array of indexes in the table
261 * @return xmldb_index[]
263 public function getIndexes() {
264 return $this->indexes
;
268 * Returns one xmldb_field
269 * @param string $fieldname
270 * @return xmldb_field|null
272 public function getField($fieldname) {
273 $i = $this->findFieldInArray($fieldname);
275 return $this->fields
[$i];
281 * Returns the position of one field in the array.
282 * @param string $fieldname
283 * @return int|null index of the field, or null if not found.
285 public function findFieldInArray($fieldname) {
286 foreach ($this->fields
as $i => $field) {
287 if ($fieldname == $field->getName()) {
295 * This function will reorder the array of fields
296 * @return bool whether the reordering succeeded.
298 public function orderFields() {
299 $result = $this->orderElements($this->fields
);
301 $this->setFields($result);
309 * Returns one xmldb_key
310 * @param string $keyname
311 * @return xmldb_key|null
313 public function getKey($keyname) {
314 $i = $this->findKeyInArray($keyname);
316 return $this->keys
[$i];
322 * Returns the position of one key in the array.
323 * @param string $keyname
324 * @return int|null index of the key, or null if not found.
326 public function findKeyInArray($keyname) {
327 foreach ($this->keys
as $i => $key) {
328 if ($keyname == $key->getName()) {
336 * This function will reorder the array of keys
337 * @return bool whether the reordering succeeded.
339 public function orderKeys() {
340 $result = $this->orderElements($this->keys
);
342 $this->setKeys($result);
350 * Returns one xmldb_index
351 * @param string $indexname
352 * @return xmldb_index|null
354 public function getIndex($indexname) {
355 $i = $this->findIndexInArray($indexname);
357 return $this->indexes
[$i];
363 * Returns the position of one index in the array.
364 * @param string $indexname
365 * @return int|null index of the index, or null if not found.
367 public function findIndexInArray($indexname) {
368 foreach ($this->indexes
as $i => $index) {
369 if ($indexname == $index->getName()) {
377 * This function will reorder the array of indexes
378 * @return bool whether the reordering succeeded.
380 public function orderIndexes() {
381 $result = $this->orderElements($this->indexes
);
383 $this->setIndexes($result);
391 * This function will set the array of fields in the table
392 * @param xmldb_field[] $fields
394 public function setFields($fields) {
395 $this->fields
= $fields;
399 * This function will set the array of keys in the table
400 * @param xmldb_key[] $keys
402 public function setKeys($keys) {
407 * This function will set the array of indexes in the table
408 * @param xmldb_index[] $indexes
410 public function setIndexes($indexes) {
411 $this->indexes
= $indexes;
415 * Delete one field from the table
416 * @param string $fieldname
418 public function deleteField($fieldname) {
420 $field = $this->getField($fieldname);
422 $i = $this->findFieldInArray($fieldname);
423 // Look for prev and next field
424 $prevfield = $this->getField($field->getPrevious());
425 $nextfield = $this->getField($field->getNext());
426 // Change their previous and next attributes
428 $prevfield->setNext($field->getNext());
431 $nextfield->setPrevious($field->getPrevious());
434 unset($this->fields
[$i]);
435 // Reorder the whole structure
436 $this->orderFields($this->fields
);
437 // Recalculate the hash
438 $this->calculateHash(true);
439 // We have one deleted field, so the table has changed
440 $this->setChanged(true);
445 * Delete one key from the table
446 * @param string $keyname
448 public function deleteKey($keyname) {
450 $key = $this->getKey($keyname);
452 $i = $this->findKeyInArray($keyname);
453 // Look for prev and next key
454 $prevkey = $this->getKey($key->getPrevious());
455 $nextkey = $this->getKey($key->getNext());
456 // Change their previous and next attributes
458 $prevkey->setNext($key->getNext());
461 $nextkey->setPrevious($key->getPrevious());
464 unset($this->keys
[$i]);
466 $this->orderKeys($this->keys
);
467 // Recalculate the hash
468 $this->calculateHash(true);
469 // We have one deleted key, so the table has changed
470 $this->setChanged(true);
475 * Delete one index from the table
476 * @param string $indexname
478 public function deleteIndex($indexname) {
480 $index = $this->getIndex($indexname);
482 $i = $this->findIndexInArray($indexname);
483 // Look for prev and next index
484 $previndex = $this->getIndex($index->getPrevious());
485 $nextindex = $this->getIndex($index->getNext());
486 // Change their previous and next attributes
488 $previndex->setNext($index->getNext());
491 $nextindex->setPrevious($index->getPrevious());
494 unset($this->indexes
[$i]);
495 // Reorder the indexes
496 $this->orderIndexes($this->indexes
);
497 // Recalculate the hash
498 $this->calculateHash(true);
499 // We have one deleted index, so the table has changed
500 $this->setChanged(true);
505 * Load data from XML to the table
506 * @param array $xmlarr
507 * @return bool success
509 public function arr2xmldb_table($xmlarr) {
516 // traverse_xmlize($xmlarr); //Debug
517 // print_object ($GLOBALS['traverse_array']); //Debug
518 // $GLOBALS['traverse_array']=""; //Debug
520 // Process table attributes (name, comment, previoustable and nexttable)
521 if (isset($xmlarr['@']['NAME'])) {
522 $this->name
= trim($xmlarr['@']['NAME']);
524 $this->errormsg
= 'Missing NAME attribute';
525 $this->debug($this->errormsg
);
528 if (isset($xmlarr['@']['COMMENT'])) {
529 $this->comment
= trim($xmlarr['@']['COMMENT']);
530 } else if (!empty($CFG->xmldbdisablecommentchecking
)) {
533 $this->errormsg
= 'Missing COMMENT attribute';
534 $this->debug($this->errormsg
);
538 // Iterate over fields
539 if (isset($xmlarr['#']['FIELDS']['0']['#']['FIELD'])) {
540 foreach ($xmlarr['#']['FIELDS']['0']['#']['FIELD'] as $xmlfield) {
541 if (!$result) { //Skip on error
544 $name = trim($xmlfield['@']['NAME']);
545 $field = new xmldb_field($name);
546 $field->arr2xmldb_field($xmlfield);
547 $this->fields
[] = $field;
548 if (!$field->isLoaded()) {
549 $this->errormsg
= 'Problem loading field ' . $name;
550 $this->debug($this->errormsg
);
555 $this->errormsg
= 'Missing FIELDS section';
556 $this->debug($this->errormsg
);
560 // Perform some general checks over fields
561 if ($result && $this->fields
) {
562 // Check field names are ok (lowercase, a-z _-)
563 if (!$this->checkNameValues($this->fields
)) {
564 $this->errormsg
= 'Some FIELDS name values are incorrect';
565 $this->debug($this->errormsg
);
568 // Compute prev/next.
569 $this->fixPrevNext($this->fields
);
571 if ($result && !$this->orderFields($this->fields
)) {
572 $this->errormsg
= 'Error ordering the fields';
573 $this->debug($this->errormsg
);
579 if (isset($xmlarr['#']['KEYS']['0']['#']['KEY'])) {
580 foreach ($xmlarr['#']['KEYS']['0']['#']['KEY'] as $xmlkey) {
581 if (!$result) { //Skip on error
584 $name = trim($xmlkey['@']['NAME']);
585 $key = new xmldb_key($name);
586 $key->arr2xmldb_key($xmlkey);
587 $this->keys
[] = $key;
588 if (!$key->isLoaded()) {
589 $this->errormsg
= 'Problem loading key ' . $name;
590 $this->debug($this->errormsg
);
595 $this->errormsg
= 'Missing KEYS section (at least one PK must exist)';
596 $this->debug($this->errormsg
);
600 // Perform some general checks over keys
601 if ($result && $this->keys
) {
602 // Check keys names are ok (lowercase, a-z _-)
603 if (!$this->checkNameValues($this->keys
)) {
604 $this->errormsg
= 'Some KEYS name values are incorrect';
605 $this->debug($this->errormsg
);
608 // Compute prev/next.
609 $this->fixPrevNext($this->keys
);
611 if ($result && !$this->orderKeys($this->keys
)) {
612 $this->errormsg
= 'Error ordering the keys';
613 $this->debug($this->errormsg
);
617 // TODO: Not keys with repeated fields
618 // TODO: Check fields and reffieds exist in table
621 // Iterate over indexes
622 if (isset($xmlarr['#']['INDEXES']['0']['#']['INDEX'])) {
623 foreach ($xmlarr['#']['INDEXES']['0']['#']['INDEX'] as $xmlindex) {
624 if (!$result) { //Skip on error
627 $name = trim($xmlindex['@']['NAME']);
628 $index = new xmldb_index($name);
629 $index->arr2xmldb_index($xmlindex);
630 $this->indexes
[] = $index;
631 if (!$index->isLoaded()) {
632 $this->errormsg
= 'Problem loading index ' . $name;
633 $this->debug($this->errormsg
);
639 // Perform some general checks over indexes
640 if ($result && $this->indexes
) {
641 // Check field names are ok (lowercase, a-z _-)
642 if (!$this->checkNameValues($this->indexes
)) {
643 $this->errormsg
= 'Some INDEXES name values are incorrect';
644 $this->debug($this->errormsg
);
647 // Compute prev/next.
648 $this->fixPrevNext($this->indexes
);
650 if ($result && !$this->orderIndexes($this->indexes
)) {
651 $this->errormsg
= 'Error ordering the indexes';
652 $this->debug($this->errormsg
);
655 // TODO: Not indexes with repeated fields
656 // TODO: Check fields exist in table
659 // Set some attributes
661 $this->loaded
= true;
663 $this->calculateHash();
668 * This function calculate and set the hash of one xmldb_table
669 * @param bool $recursive
671 public function calculateHash($recursive = false) {
672 if (!$this->loaded
) {
675 $key = $this->name
. $this->comment
;
677 foreach ($this->fields
as $fie) {
678 $field = $this->getField($fie->getName());
680 $field->calculateHash($recursive);
682 $key .= $field->getHash();
686 foreach ($this->keys
as $ke) {
687 $k = $this->getKey($ke->getName());
689 $k->calculateHash($recursive);
691 $key .= $k->getHash();
694 if ($this->indexes
) {
695 foreach ($this->indexes
as $in) {
696 $index = $this->getIndex($in->getName());
698 $index->calculateHash($recursive);
700 $key .= $index->getHash();
703 $this->hash
= md5($key);
708 * Validates the table restrictions (does not validate child elements).
710 * The error message should not be localised because it is intended for developers,
711 * end users and admins should never see these problems!
713 * @param xmldb_table $xmldb_table optional when object is table
714 * @return string null if ok, error message if problem found
716 public function validateDefinition(xmldb_table
$xmldb_table=null) {
717 // table parameter is ignored
718 $name = $this->getName();
719 if (strlen($name) > self
::NAME_MAX_LENGTH
) {
720 return 'Invalid table name {'.$name.'}: name is too long. Limit is '.self
::NAME_MAX_LENGTH
.' chars.';
722 if (!preg_match('/^[a-z][a-z0-9_]*$/', $name)) {
723 return 'Invalid table name {'.$name.'}: name includes invalid characters.';
730 * This function will output the XML text for one table
733 public function xmlOutput() {
735 $o.= ' <TABLE NAME="' . $this->name
. '"';
736 if ($this->comment
) {
737 $o.= ' COMMENT="' . htmlspecialchars($this->comment
, ENT_COMPAT
) . '"';
742 $o.= ' <FIELDS>' . "\n";
743 foreach ($this->fields
as $field) {
744 $o.= $field->xmlOutput();
746 $o.= ' </FIELDS>' . "\n";
750 $o.= ' <KEYS>' . "\n";
751 foreach ($this->keys
as $key) {
752 $o.= $key->xmlOutput();
754 $o.= ' </KEYS>' . "\n";
757 if ($this->indexes
) {
758 $o.= ' <INDEXES>' . "\n";
759 foreach ($this->indexes
as $index) {
760 $o.= $index->xmlOutput();
762 $o.= ' </INDEXES>' . "\n";
764 $o.= ' </TABLE>' . "\n";
770 * This function will add one new field to the table with all
771 * its attributes defined
773 * @param string $name name of the field
774 * @param int $type XMLDB_TYPE_INTEGER, XMLDB_TYPE_NUMBER, XMLDB_TYPE_CHAR, XMLDB_TYPE_TEXT, XMLDB_TYPE_BINARY
775 * @param string $precision length for integers and chars, two-comma separated numbers for numbers
776 * @param bool $unsigned XMLDB_UNSIGNED or null (or false)
777 * @param bool $notnull XMLDB_NOTNULL or null (or false)
778 * @param bool $sequence XMLDB_SEQUENCE or null (or false)
779 * @param mixed $default meaningful default o null (or false)
780 * @param xmldb_object $previous name of the previous field in the table or null (or false)
781 * @return xmlddb_field
783 public function add_field($name, $type, $precision=null, $unsigned=null, $notnull=null, $sequence=null, $default=null, $previous=null) {
784 $field = new xmldb_field($name, $type, $precision, $unsigned, $notnull, $sequence, $default);
785 $this->addField($field, $previous);
791 * This function will add one new key to the table with all
792 * its attributes defined
794 * @param string $name name of the key
795 * @param int $type XMLDB_KEY_PRIMARY, XMLDB_KEY_UNIQUE, XMLDB_KEY_FOREIGN
796 * @param array $fields an array of fieldnames to build the key over
797 * @param string $reftable name of the table the FK points to or null
798 * @param array $reffields an array of fieldnames in the FK table or null
800 public function add_key($name, $type, $fields, $reftable=null, $reffields=null) {
801 $key = new xmldb_key($name, $type, $fields, $reftable, $reffields);
806 * This function will add one new index to the table with all
807 * its attributes defined
809 * @param string $name name of the index
810 * @param int $type XMLDB_INDEX_UNIQUE, XMLDB_INDEX_NOTUNIQUE
811 * @param array $fields an array of fieldnames to build the index over
812 * @param array $hints optional index type hints
814 public function add_index($name, $type, $fields, $hints = array()) {
815 $index = new xmldb_index($name, $type, $fields, $hints);
816 $this->addIndex($index);
820 * This function will return all the errors found in one table
821 * looking recursively inside each field/key/index. Returns
822 * an array of errors or false
824 public function getAllErrors() {
827 // First the table itself
828 if ($this->getError()) {
829 $errors[] = $this->getError();
831 // Delegate to fields
832 if ($fields = $this->getFields()) {
833 foreach ($fields as $field) {
834 if ($field->getError()) {
835 $errors[] = $field->getError();
840 if ($keys = $this->getKeys()) {
841 foreach ($keys as $key) {
842 if ($key->getError()) {
843 $errors[] = $key->getError();
847 // Delegate to indexes
848 if ($indexes = $this->getIndexes()) {
849 foreach ($indexes as $index) {
850 if ($index->getError()) {
851 $errors[] = $index->getError();
856 if (count($errors)) {