1 <?php
  2 
  3 /**
  4  * Class to represent sql tables.
  5  *
  6  * @author gharlan
  7  *
  8  * @package redaxo\core\sql
  9  */
 10 class rex_sql_table
 11 {
 12     use rex_instance_pool_trait;
 13 
 14     const FIRST = 'FIRST '; // The space is intended: column names cannot end with space
 15 
 16     /** @var rex_sql */
 17     private $sql;
 18 
 19     /** @var bool */
 20     private $new;
 21 
 22     /** @var string */
 23     private $name;
 24 
 25     /** @var string */
 26     private $originalName;
 27 
 28     /** @var rex_sql_column[] */
 29     private $columns = [];
 30 
 31     /** @var string[] mapping from current (new) name to existing (old) name in database */
 32     private $columnsExisting = [];
 33 
 34     /** @var string[] */
 35     private $implicitOrder = [];
 36 
 37     /** @var string[] */
 38     private $positions = [];
 39 
 40     /** @var string[] */
 41     private $primaryKey = [];
 42 
 43     /** @var bool */
 44     private $primaryKeyModified = false;
 45 
 46     /** @var rex_sql_index[] */
 47     private $indexes = [];
 48 
 49     /** @var string[] mapping from current (new) name to existing (old) name in database */
 50     private $indexesExisting = [];
 51 
 52     /** @var rex_sql_foreign_key[] */
 53     private $foreignKeys = [];
 54 
 55     /** @var string[] mapping from current (new) name to existing (old) name in database */
 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             // Error code 42S02 means: Table does not exist
 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      * @param string $name
145      *
146      * @return self
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      * @return bool
157      */
158     public function exists()
159     {
160         return !$this->new;
161     }
162 
163     /**
164      * @return string
165      */
166     public function getName()
167     {
168         return $this->name;
169     }
170 
171     /**
172      * @param string $name
173      *
174      * @return $this
175      */
176     public function setName($name)
177     {
178         $this->name = $name;
179 
180         return $this;
181     }
182 
183     /**
184      * @param string $name
185      *
186      * @return bool
187      */
188     public function hasColumn($name)
189     {
190         return isset($this->columns[$name]);
191     }
192 
193     /**
194      * @param string $name
195      *
196      * @return null|rex_sql_column
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      * @return rex_sql_column[]
209      */
210     public function getColumns()
211     {
212         return $this->columns;
213     }
214 
215     /**
216      * @param rex_sql_column $column
217      * @param null|string    $afterColumn Column name or `rex_sql_table::FIRST`
218      *
219      * @return $this
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      * @param rex_sql_column $column
238      * @param null|string    $afterColumn Column name or `rex_sql_table::FIRST`
239      *
240      * @return $this
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      * @return $this
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      * @return $this
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      * @param string $oldName
287      * @param string $newName
288      *
289      * @return $this
290      *
291      * @throws rex_exception
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      * @param string $name
327      *
328      * @return $this
329      */
330     public function removeColumn($name)
331     {
332         unset($this->columns[$name]);
333 
334         return $this;
335     }
336 
337     /**
338      * @return null|string[] Column names
339      */
340     public function getPrimaryKey()
341     {
342         return $this->primaryKey ?: null;
343     }
344 
345     /**
346      * @param null|string|string[] $columns Column name(s)
347      *
348      * @return $this
349      *
350      * @throws rex_exception
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      * @param string $name
372      *
373      * @return bool
374      */
375     public function hasIndex($name)
376     {
377         return isset($this->indexes[$name]);
378     }
379 
380     /**
381      * @param string $name
382      *
383      * @return null|rex_sql_index
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      * @return rex_sql_index[]
396      */
397     public function getIndexes()
398     {
399         return $this->indexes;
400     }
401 
402     /**
403      * @param rex_sql_index $index
404      *
405      * @return $this
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      * @param rex_sql_index $index
422      *
423      * @return $this
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      * @param string $oldName
444      * @param string $newName
445      *
446      * @return $this
447      *
448      * @throws rex_exception
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      * @param string $name
479      *
480      * @return $this
481      */
482     public function removeIndex($name)
483     {
484         unset($this->indexes[$name]);
485 
486         return $this;
487     }
488 
489     /**
490      * @param string $name
491      *
492      * @return bool
493      */
494     public function hasForeignKey($name)
495     {
496         return isset($this->foreignKeys[$name]);
497     }
498 
499     /**
500      * @param string $name
501      *
502      * @return null|rex_sql_foreign_key
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      * @return rex_sql_foreign_key[]
515      */
516     public function getForeignKeys()
517     {
518         return $this->foreignKeys;
519     }
520 
521     /**
522      * @return $this
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      * @return $this
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      * @param string $oldName
559      * @param string $newName
560      *
561      * @return $this
562      *
563      * @throws rex_exception
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      * @param string $name
594      *
595      * @return $this
596      */
597     public function removeForeignKey($name)
598     {
599         unset($this->foreignKeys[$name]);
600 
601         return $this;
602     }
603 
604     /**
605      * Ensures that the table exists with the given definition.
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             // unset is necessary to add new position as last array element
630             unset($this->positions[$name]);
631             $this->positions[$name] = $after;
632         }
633 
634         $this->alter();
635     }
636 
637     /**
638      * Drops the table if it exists.
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      * Creates the table.
656      *
657      * @throws rex_exception
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      * Alters the table.
699      *
700      * @throws rex_exception
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         // unset is necessary to add new position as last array element
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