1 <?php
2
3 4 5 6 7 8 9
10 class rex_sql_table
11 {
12 use rex_instance_pool_trait;
13
14 const FIRST = 'FIRST ';
15
16
17 private $sql;
18
19
20 private $new;
21
22
23 private $name;
24
25
26 private $originalName;
27
28
29 private $columns = [];
30
31
32 private $columnsExisting = [];
33
34
35 private $implicitOrder = [];
36
37
38 private $positions = [];
39
40
41 private $primaryKey = [];
42
43
44 private $primaryKeyModified = false;
45
46
47 private $indexes = [];
48
49
50 private $indexesExisting = [];
51
52
53 private $foreignKeys = [];
54
55
56 private $foreignKeysExisting = [];
57
58 private function __construct($name)
59 {
60 $this->sql = rex_sql::factory();
61 $this->name = $name;
62 $this->originalName = $name;
63
64 try {
65 $columns = rex_sql::showColumns($name);
66 $this->new = false;
67 } catch (rex_sql_exception $exception) {
68
69 if ($exception->getSql() && '42S02' !== $exception->getSql()->getErrno()) {
70 throw $exception;
71 }
72
73 $this->new = true;
74
75 return;
76 }
77
78 foreach ($columns as $column) {
79 $this->columns[$column['name']] = new rex_sql_column(
80 $column['name'],
81 $column['type'],
82 'YES' === $column['null'],
83 $column['default'],
84 $column['extra'] ?: null
85 );
86
87 $this->columnsExisting[$column['name']] = $column['name'];
88
89 if ('PRI' === $column['key']) {
90 $this->primaryKey[] = $column['name'];
91 }
92 }
93
94 $indexParts = $this->sql->getArray('SHOW INDEXES FROM '.$this->sql->escapeIdentifier($name));
95 $indexes = [];
96 foreach ($indexParts as $part) {
97 if ('PRIMARY' !== $part['Key_name']) {
98 $indexes[$part['Key_name']][] = $part;
99 }
100 }
101
102 foreach ($indexes as $indexName => $parts) {
103 $columns = [];
104 foreach ($parts as $part) {
105 $columns[] = $part['Column_name'];
106 }
107
108 if ('FULLTEXT' === $parts[0]['Index_type']) {
109 $type = rex_sql_index::FULLTEXT;
110 } elseif (0 === (int) $parts[0]['Non_unique']) {
111 $type = rex_sql_index::UNIQUE;
112 } else {
113 $type = rex_sql_index::INDEX;
114 }
115
116 $this->indexes[$indexName] = new rex_sql_index($indexName, $columns, $type);
117 $this->indexesExisting[$indexName] = $indexName;
118 }
119
120 $foreignKeyParts = $this->sql->getArray('
121 SELECT c.constraint_name, c.referenced_table_name, c.update_rule, c.delete_rule, k.column_name, k.referenced_column_name
122 FROM information_schema.referential_constraints c
123 LEFT JOIN information_schema.key_column_usage k ON c.constraint_name = k.constraint_name
124 WHERE c.constraint_schema = DATABASE() AND c.table_name = ?', [$name]);
125 $foreignKeys = [];
126 foreach ($foreignKeyParts as $part) {
127 $foreignKeys[$part['constraint_name']][] = $part;
128 }
129
130 foreach ($foreignKeys as $fkName => $parts) {
131 $columns = [];
132 foreach ($parts as $part) {
133 $columns[$part['column_name']] = $part['referenced_column_name'];
134 }
135
136 $fk = $parts[0];
137
138 $this->foreignKeys[$fkName] = new rex_sql_foreign_key($fkName, $fk['referenced_table_name'], $columns, $fk['update_rule'], $fk['delete_rule']);
139 $this->foreignKeysExisting[$fkName] = $fkName;
140 }
141 }
142
143 144 145 146 147
148 public static function get($name)
149 {
150 return self::getInstance($name, function ($name) {
151 return new self($name);
152 });
153 }
154
155 156 157
158 public function exists()
159 {
160 return !$this->new;
161 }
162
163 164 165
166 public function getName()
167 {
168 return $this->name;
169 }
170
171 172 173 174 175
176 public function setName($name)
177 {
178 $this->name = $name;
179
180 return $this;
181 }
182
183 184 185 186 187
188 public function hasColumn($name)
189 {
190 return isset($this->columns[$name]);
191 }
192
193 194 195 196 197
198 public function getColumn($name)
199 {
200 if (!$this->hasColumn($name)) {
201 return null;
202 }
203
204 return $this->columns[$name];
205 }
206
207 208 209
210 public function getColumns()
211 {
212 return $this->columns;
213 }
214
215 216 217 218 219 220
221 public function addColumn(rex_sql_column $column, $afterColumn = null)
222 {
223 $name = $column->getName();
224
225 if ($this->hasColumn($name)) {
226 throw new RuntimeException(sprintf('Column "%s" already exists.', $name));
227 }
228
229 $this->columns[$name] = $column;
230
231 $this->setPosition($name, $afterColumn);
232
233 return $this;
234 }
235
236 237 238 239 240 241
242 public function ensureColumn(rex_sql_column $column, $afterColumn = null)
243 {
244 $name = $column->getName();
245
246 if (!$this->hasColumn($name)) {
247 return $this->addColumn($column, $afterColumn);
248 }
249
250 $this->setPosition($name, $afterColumn);
251
252 if ($this->getColumn($name)->equals($column)) {
253 return $this;
254 }
255
256 $this->columns[$name] = $column->setModified(true);
257
258 return $this;
259 }
260
261 262 263
264 public function ensurePrimaryIdColumn()
265 {
266 return $this
267 ->ensureColumn(new rex_sql_column('id', 'int(10) unsigned', false, null, 'auto_increment'))
268 ->setPrimaryKey('id')
269 ;
270 }
271
272 273 274
275 public function ensureGlobalColumns()
276 {
277 return $this
278 ->ensureColumn(new rex_sql_column('createdate', 'datetime'))
279 ->ensureColumn(new rex_sql_column('createuser', 'varchar(255)'))
280 ->ensureColumn(new rex_sql_column('updatedate', 'datetime'))
281 ->ensureColumn(new rex_sql_column('updateuser', 'varchar(255)'))
282 ;
283 }
284
285 286 287 288 289 290 291 292
293 public function renameColumn($oldName, $newName)
294 {
295 if (!$this->hasColumn($oldName)) {
296 throw new rex_exception(sprintf('Column with name "%s" does not exist.', $oldName));
297 }
298
299 if ($this->hasColumn($newName)) {
300 throw new rex_exception(sprintf('Column with the new name "%s" already exists.', $newName));
301 }
302
303 if ($oldName === $newName) {
304 return $this;
305 }
306
307 $column = $this->getColumn($oldName)->setName($newName);
308
309 unset($this->columns[$oldName]);
310 $this->columns[$newName] = $column;
311
312 if (isset($this->columnsExisting[$oldName])) {
313 $this->columnsExisting[$newName] = $this->columnsExisting[$oldName];
314 unset($this->columnsExisting[$oldName]);
315 }
316
317 if (false !== $key = array_search($oldName, $this->primaryKey)) {
318 $this->primaryKey[$key] = $newName;
319 $this->primaryKeyModified = true;
320 }
321
322 return $this;
323 }
324
325 326 327 328 329
330 public function removeColumn($name)
331 {
332 unset($this->columns[$name]);
333
334 return $this;
335 }
336
337 338 339
340 public function getPrimaryKey()
341 {
342 return $this->primaryKey ?: null;
343 }
344
345 346 347 348 349 350 351
352 public function setPrimaryKey($columns)
353 {
354 if (is_array($columns) && !$columns) {
355 throw new rex_exception('The primary key column array can not be empty. To delete the primary key use `null` instead.');
356 }
357
358 $columns = null === $columns ? [] : (array) $columns;
359
360 if ($this->primaryKey === $columns) {
361 return $this;
362 }
363
364 $this->primaryKey = (array) $columns;
365 $this->primaryKeyModified = true;
366
367 return $this;
368 }
369
370 371 372 373 374
375 public function hasIndex($name)
376 {
377 return isset($this->indexes[$name]);
378 }
379
380 381 382 383 384
385 public function getIndex($name)
386 {
387 if (!$this->hasIndex($name)) {
388 return null;
389 }
390
391 return $this->indexes[$name];
392 }
393
394 395 396
397 public function getIndexes()
398 {
399 return $this->indexes;
400 }
401
402 403 404 405 406
407 public function addIndex(rex_sql_index $index)
408 {
409 $name = $index->getName();
410
411 if ($this->hasIndex($name)) {
412 throw new RuntimeException(sprintf('Index "%s" already exists.', $name));
413 }
414
415 $this->indexes[$name] = $index;
416
417 return $this;
418 }
419
420 421 422 423 424
425 public function ensureIndex(rex_sql_index $index)
426 {
427 $name = $index->getName();
428
429 if (!$this->hasIndex($name)) {
430 return $this->addIndex($index);
431 }
432
433 if ($this->getIndex($name)->equals($index)) {
434 return $this;
435 }
436
437 $this->indexes[$name] = $index->setModified(true);
438
439 return $this;
440 }
441
442 443 444 445 446 447 448 449
450 public function renameIndex($oldName, $newName)
451 {
452 if (!$this->hasIndex($oldName)) {
453 throw new rex_exception(sprintf('Index with name "%s" does not exist.', $oldName));
454 }
455
456 if ($this->hasIndex($newName)) {
457 throw new rex_exception(sprintf('Index with the new name "%s" already exists.', $newName));
458 }
459
460 if ($oldName === $newName) {
461 return $this;
462 }
463
464 $index = $this->getIndex($oldName)->setName($newName);
465
466 unset($this->indexes[$oldName]);
467 $this->indexes[$newName] = $index;
468
469 if (isset($this->indexesExisting[$oldName])) {
470 $this->indexesExisting[$newName] = $this->indexesExisting[$oldName];
471 unset($this->indexesExisting[$oldName]);
472 }
473
474 return $this;
475 }
476
477 478 479 480 481
482 public function removeIndex($name)
483 {
484 unset($this->indexes[$name]);
485
486 return $this;
487 }
488
489 490 491 492 493
494 public function hasForeignKey($name)
495 {
496 return isset($this->foreignKeys[$name]);
497 }
498
499 500 501 502 503
504 public function getForeignKey($name)
505 {
506 if (!$this->hasForeignKey($name)) {
507 return null;
508 }
509
510 return $this->foreignKeys[$name];
511 }
512
513 514 515
516 public function getForeignKeys()
517 {
518 return $this->foreignKeys;
519 }
520
521 522 523
524 public function addForeignKey(rex_sql_foreign_key $foreignKey)
525 {
526 $name = $foreignKey->getName();
527
528 if ($this->hasForeignKey($name)) {
529 throw new RuntimeException(sprintf('Foreign key "%s" already exists.', $name));
530 }
531
532 $this->foreignKeys[$name] = $foreignKey;
533
534 return $this;
535 }
536
537 538 539
540 public function ensureForeignKey(rex_sql_foreign_key $foreignKey)
541 {
542 $name = $foreignKey->getName();
543
544 if (!$this->hasForeignKey($name)) {
545 return $this->addForeignKey($foreignKey);
546 }
547
548 if ($this->getForeignKey($name)->equals($foreignKey)) {
549 return $this;
550 }
551
552 $this->foreignKeys[$name] = $foreignKey->setModified(true);
553
554 return $this;
555 }
556
557 558 559 560 561 562 563 564
565 public function renameForeignKey($oldName, $newName)
566 {
567 if (!$this->hasForeignKey($oldName)) {
568 throw new rex_exception(sprintf('Foreign key with name "%s" does not exist.', $oldName));
569 }
570
571 if ($this->hasForeignKey($newName)) {
572 throw new rex_exception(sprintf('Foreign key with the new name "%s" already exists.', $newName));
573 }
574
575 if ($oldName === $newName) {
576 return $this;
577 }
578
579 $foreignKey = $this->getForeignKey($oldName)->setName($newName);
580
581 unset($this->foreignKeys[$oldName]);
582 $this->foreignKeys[$newName] = $foreignKey;
583
584 if (isset($this->foreignKeysExisting[$oldName])) {
585 $this->foreignKeysExisting[$newName] = $this->foreignKeysExisting[$oldName];
586 unset($this->foreignKeysExisting[$oldName]);
587 }
588
589 return $this;
590 }
591
592 593 594 595 596
597 public function removeForeignKey($name)
598 {
599 unset($this->foreignKeys[$name]);
600
601 return $this;
602 }
603
604 605 606
607 public function ensure()
608 {
609 if ($this->new) {
610 $this->create();
611
612 return;
613 }
614
615 $positions = $this->positions;
616 $this->positions = [];
617
618 $previous = self::FIRST;
619 foreach ($this->implicitOrder as $name) {
620 if (isset($this->positions[$name])) {
621 continue;
622 }
623
624 $this->positions[$name] = $previous;
625 $previous = $name;
626 }
627
628 foreach ($positions as $name => $after) {
629
630 unset($this->positions[$name]);
631 $this->positions[$name] = $after;
632 }
633
634 $this->alter();
635 }
636
637 638 639
640 public function drop()
641 {
642 if (!$this->new) {
643 $this->sql->setQuery(sprintf('DROP TABLE %s', $this->sql->escapeIdentifier($this->name)));
644 }
645
646 $this->new = true;
647 $this->originalName = $this->name;
648 $this->columnsExisting = [];
649 $this->implicitOrder = [];
650 $this->positions = [];
651 $this->primaryKeyModified = !empty($this->primaryKey);
652 }
653
654 655 656 657 658
659 public function create()
660 {
661 if (!$this->new) {
662 throw new rex_exception(sprintf('Table "%s" already exists.', $this->name));
663 }
664 if (!$this->columns) {
665 throw new rex_exception('A table must have at least one column.');
666 }
667
668 $this->sortColumns();
669
670 $parts = [];
671
672 foreach ($this->columns as $column) {
673 $parts[] = $this->getColumnDefinition($column);
674 }
675
676 if ($this->primaryKey) {
677 $parts[] = 'PRIMARY KEY '.$this->getKeyColumnsDefintion($this->primaryKey);
678 }
679
680 foreach ($this->indexes as $index) {
681 $parts[] = $this->getIndexDefinition($index);
682 }
683
684 foreach ($this->foreignKeys as $foreignKey) {
685 $parts[] = $this->getForeignKeyDefinition($foreignKey);
686 }
687
688 $query = 'CREATE TABLE '.$this->sql->escapeIdentifier($this->name)." (\n ";
689 $query .= implode(",\n ", $parts);
690 $query .= "\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
691
692 $this->sql->setQuery($query);
693
694 $this->resetModified();
695 }
696
697 698 699 700 701
702 public function alter()
703 {
704 if ($this->new) {
705 throw new rex_exception(sprintf('Table "%s" does not exist.', $this->name));
706 }
707
708 $parts = [];
709 $dropForeignKeys = [];
710
711 if ($this->name !== $this->originalName) {
712 $parts[] = 'RENAME '.$this->sql->escapeIdentifier($this->name);
713 }
714
715 if ($this->primaryKeyModified) {
716 $parts[] = 'DROP PRIMARY KEY';
717 }
718
719 foreach ($this->indexesExisting as $newName => $oldName) {
720 if (!isset($this->indexes[$newName]) || $this->indexes[$newName]->isModified()) {
721 $parts[] = 'DROP INDEX '.$this->sql->escapeIdentifier($oldName);
722 }
723 }
724
725 foreach ($this->foreignKeysExisting as $newName => $oldName) {
726 if (!isset($this->foreignKeys[$newName]) || $this->foreignKeys[$newName]->isModified()) {
727 $dropForeignKeys[] = 'DROP FOREIGN KEY '.$this->sql->escapeIdentifier($oldName);
728 }
729 }
730
731 $columns = $this->columns;
732 $columnsExisting = $this->columnsExisting;
733
734 $handle = function ($name, $after = null) use (&$parts, &$columns, &$columnsExisting) {
735 $column = $columns[$name];
736 $new = !isset($columnsExisting[$name]);
737 $oldName = $new ? null : $columnsExisting[$name];
738 unset($columns[$name], $columnsExisting[$name]);
739
740 if (!$new && !$column->isModified() && null === $after) {
741 return;
742 }
743
744 $definition = $this->getColumnDefinition($column);
745
746 if (self::FIRST === $after) {
747 $definition .= ' FIRST';
748 } elseif (null !== $after) {
749 $definition .= ' AFTER '.$this->sql->escapeIdentifier($after);
750 }
751
752 if ($new) {
753 $parts[] = 'ADD '.$definition;
754 } else {
755 $parts[] = 'CHANGE '.$this->sql->escapeIdentifier($oldName).' '.$definition;
756 }
757 };
758
759 $currentOrder = [];
760 $after = self::FIRST;
761 foreach ($columns as $name => $column) {
762 $currentOrder[$after] = $name;
763 $after = $name;
764
765 if (!isset($this->positions[$name])) {
766 $handle($name);
767 }
768 }
769
770 foreach ($this->positions as $name => $after) {
771 if (!isset($columns[$name])) {
772 continue;
773 }
774
775 if (isset($currentOrder[$after]) && $currentOrder[$after] === $name) {
776 $after = null;
777 } else {
778 unset($currentOrder[$name]);
779 }
780
781 $handle($name, $after);
782 }
783
784 foreach ($columnsExisting as $oldName) {
785 $parts[] = 'DROP '.$this->sql->escapeIdentifier($oldName);
786 }
787
788 if ($this->primaryKeyModified && $this->primaryKey) {
789 $parts[] = 'ADD PRIMARY KEY '.$this->getKeyColumnsDefintion($this->primaryKey);
790 }
791
792 $fulltextIndexes = [];
793 $fulltextAdded = false;
794 foreach ($this->indexes as $index) {
795 if (!$index->isModified() && isset($this->indexesExisting[$index->getName()])) {
796 continue;
797 }
798
799 if (rex_sql_index::FULLTEXT === $index->getType()) {
800 if ($fulltextAdded) {
801 $fulltextIndexes[] = 'ADD '.$this->getIndexDefinition($index);
802
803 continue;
804 }
805
806 $fulltextAdded = true;
807 }
808
809 $parts[] = 'ADD '.$this->getIndexDefinition($index);
810 }
811
812 foreach ($this->foreignKeys as $foreignKey) {
813 if ($foreignKey->isModified() || !isset($this->foreignKeysExisting[$foreignKey->getName()])) {
814 $parts[] = 'ADD '.$this->getForeignKeyDefinition($foreignKey);
815 }
816 }
817
818 if (!$parts && !$dropForeignKeys) {
819 return;
820 }
821
822 foreach ([$dropForeignKeys, $parts] as $stepParts) {
823 if ($stepParts) {
824 $query = 'ALTER TABLE '.$this->sql->escapeIdentifier($this->originalName)."\n ";
825 $query .= implode(",\n ", $stepParts);
826 $query .= ';';
827
828 $this->sql->setQuery($query);
829 }
830 }
831
832 foreach ($fulltextIndexes as $fulltextIndex) {
833 $this->sql->setQuery('ALTER TABLE '.$this->sql->escapeIdentifier($this->originalName).' '.$fulltextIndex.';');
834 }
835
836 $this->sortColumns();
837 $this->resetModified();
838 }
839
840 private function setPosition($name, $afterColumn)
841 {
842 if (null === $afterColumn) {
843 $this->implicitOrder[] = $name;
844
845 return;
846 }
847
848 if (self::FIRST !== $afterColumn && !$this->hasColumn($afterColumn)) {
849 throw new InvalidArgumentException(sprintf('Column "%s" can not be placed after "%s", because that column does not exist.', $name, $afterColumn));
850 }
851
852
853 unset($this->positions[$name]);
854 $this->positions[$name] = $afterColumn;
855 }
856
857 private function getColumnDefinition(rex_sql_column $column)
858 {
859 $default = $column->getDefault();
860 if (!$default) {
861 $default = '';
862 } elseif (
863 in_array(strtolower($column->getType()), ['timestamp', 'datetime'], true) &&
864 in_array(strtolower($default), ['current_timestamp', 'current_timestamp()'], true)
865 ) {
866 $default = 'DEFAULT '.$default;
867 } else {
868 $default = 'DEFAULT '.$this->sql->escape($column->getDefault());
869 }
870
871 return sprintf(
872 '%s %s %s %s %s',
873 $this->sql->escapeIdentifier($column->getName()),
874 $column->getType(),
875 $default,
876 $column->isNullable() ? '' : 'NOT NULL',
877 $column->getExtra()
878 );
879 }
880
881 private function getIndexDefinition(rex_sql_index $index)
882 {
883 return sprintf(
884 '%s %s %s',
885 $index->getType(),
886 $this->sql->escapeIdentifier($index->getName()),
887 $this->getKeyColumnsDefintion($index->getColumns())
888 );
889 }
890
891 private function getForeignKeyDefinition(rex_sql_foreign_key $foreignKey)
892 {
893 return sprintf(
894 'CONSTRAINT %s FOREIGN KEY %s REFERENCES %s %s ON UPDATE %s ON DELETE %s',
895 $this->sql->escapeIdentifier($foreignKey->getName()),
896 $this->getKeyColumnsDefintion(array_keys($foreignKey->getColumns())),
897 $this->sql->escapeIdentifier($foreignKey->getTable()),
898 $this->getKeyColumnsDefintion($foreignKey->getColumns()),
899 $foreignKey->getOnUpdate(),
900 $foreignKey->getOnDelete()
901 );
902 }
903
904 private function getKeyColumnsDefintion(array $columns)
905 {
906 $columns = array_map([$this->sql, 'escapeIdentifier'], $columns);
907
908 return '('.implode(', ', $columns).')';
909 }
910
911 private function sortColumns()
912 {
913 $columns = [];
914
915 foreach ($this->columns as $name => $column) {
916 if (!isset($this->positions[$name])) {
917 $columns[$name] = $column;
918 }
919 }
920
921 foreach ($this->positions as $name => $after) {
922 $insert = [$name => $this->columns[$name]];
923
924 if (self::FIRST === $after) {
925 $columns = $insert + $columns;
926
927 continue;
928 }
929
930 $offset = array_search($after, array_keys($columns)) + 1;
931 $columns = array_slice($columns, 0, $offset) + $insert + array_slice($columns, $offset);
932 }
933
934 $this->columns = $columns;
935 }
936
937 private function resetModified()
938 {
939 $this->new = false;
940
941 if ($this->originalName !== $this->name) {
942 self::clearInstance($this->originalName);
943 self::addInstance($this->name, $this);
944 }
945
946 $this->originalName = $this->name;
947
948 $columns = $this->columns;
949 $this->columns = [];
950 $this->columnsExisting = [];
951 foreach ($columns as $column) {
952 $column->setModified(false);
953 $this->columns[$column->getName()] = $column;
954 $this->columnsExisting[$column->getName()] = $column->getName();
955 }
956
957 $this->implicitOrder = [];
958 $this->positions = [];
959
960 $this->primaryKeyModified = false;
961
962 $indexes = $this->indexes;
963 $this->indexes = [];
964 $this->indexesExisting = [];
965 foreach ($indexes as $index) {
966 $index->setModified(false);
967 $this->indexes[$index->getName()] = $index;
968 $this->indexesExisting[$index->getName()] = $index->getName();
969 }
970
971 $foreignKeys = $this->foreignKeys;
972 $this->foreignKeys = [];
973 $this->foreignKeysExisting = [];
974 foreach ($foreignKeys as $foreignKey) {
975 $foreignKey->setModified(false);
976 $this->foreignKeys[$foreignKey->getName()] = $foreignKey;
977 $this->foreignKeysExisting[$foreignKey->getName()] = $foreignKey->getName();
978 }
979 }
980 }
981