2 namespace wcf\system\database\table
;
3 use wcf\data\package\Package
;
4 use wcf\system\database\editor\DatabaseEditor
;
5 use wcf\system\database\table\column\AbstractIntDatabaseTableColumn
;
6 use wcf\system\database\table\column\IDatabaseTableColumn
;
7 use wcf\system\database\table\column\TinyintDatabaseTableColumn
;
8 use wcf\system\database\table\index\DatabaseTableForeignKey
;
9 use wcf\system\database\table\index\DatabaseTableIndex
;
10 use wcf\system\database\util\PreparedStatementConditionBuilder
;
11 use wcf\system\package\SplitNodeException
;
15 * Processes a given set of changes to database tables.
17 * @author Matthias Schmidt
18 * @copyright 2001-2020 WoltLab GmbH
19 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
20 * @package WoltLabSuite\Core\System\Database\Table
23 class DatabaseTableChangeProcessor
{
25 * maps the registered database table column names to the ids of the packages they belong to
28 protected $columnPackageIDs = [];
31 * database table columns that will be added grouped by the name of the table to which they
33 * @var IDatabaseTableColumn[][]
35 protected $columnsToAdd = [];
38 * database table columns that will be altered grouped by the name of the table to which
40 * @var IDatabaseTableColumn[][]
42 protected $columnsToAlter = [];
45 * database table columns that will be dropped grouped by the name of the table from which
46 * they will be dropped
47 * @var IDatabaseTableColumn[][]
49 protected $columnsToDrop = [];
52 * database editor to apply the relevant changes to the table layouts
58 * list of all existing tables in the used database
61 protected $existingTableNames = [];
64 * existing database tables
65 * @var DatabaseTable[]
67 protected $existingTables = [];
70 * maps the registered database table index names to the ids of the packages they belong to
73 protected $indexPackageIDs = [];
76 * indices that will be added grouped by the name of the table to which they will be added
77 * @var DatabaseTableIndex[][]
79 protected $indicesToAdd = [];
82 * indices that will be dropped grouped by the name of the table from which they will be dropped
83 * @var DatabaseTableIndex[][]
85 protected $indicesToDrop = [];
88 * maps the registered database table foreign key names to the ids of the packages they belong to
91 protected $foreignKeyPackageIDs = [];
94 * foreign keys that will be added grouped by the name of the table to which they will be
96 * @var DatabaseTableForeignKey[][]
98 protected $foreignKeysToAdd = [];
101 * foreign keys that will be dropped grouped by the name of the table from which they will
103 * @var DatabaseTableForeignKey[][]
105 protected $foreignKeysToDrop = [];
108 * package that wants to apply the changes
114 * message for the split node exception thrown after the changes have been applied
117 protected $splitNodeMessage = '';
120 * layouts/layout changes of the relevant database table
121 * @var DatabaseTable[]
126 * maps the registered database table names to the ids of the packages they belong to
129 protected $tablePackageIDs = [];
132 * database table that will be created
133 * @var DatabaseTable[]
135 protected $tablesToCreate = [];
138 * database tables that will be dropped
139 * @var DatabaseTable[]
141 protected $tablesToDrop = [];
144 * Creates a new instance of `DatabaseTableChangeProcessor`.
146 * @param Package $package
147 * @param DatabaseTable[] $tables
148 * @param DatabaseEditor $dbEditor
150 public function __construct(Package
$package, array $tables, DatabaseEditor
$dbEditor) {
151 $this->package
= $package;
154 foreach ($tables as $table) {
155 if (!($table instanceof DatabaseTable
)) {
156 throw new \
InvalidArgumentException("Tables must be instance of '" . DatabaseTable
::class . "'");
159 $tableNames[] = $table->getName();
162 $this->tables
= $tables;
163 $this->dbEditor
= $dbEditor;
165 $this->existingTableNames
= $dbEditor->getTableNames();
167 $conditionBuilder = new PreparedStatementConditionBuilder();
168 $conditionBuilder->add('sqlTable IN (?)', [$tableNames]);
169 $conditionBuilder->add('isDone = ?', [1]);
172 FROM wcf" . WCF_N
. "_package_installation_sql_log
173 " . $conditionBuilder;
174 $statement = WCF
::getDB()->prepareStatement($sql);
175 $statement->execute($conditionBuilder->getParameters());
177 while ($row = $statement->fetchArray()) {
178 if ($row['sqlIndex'] === '' && $row['sqlColumn'] === '') {
179 $this->tablePackageIDs
[$row['sqlTable']] = $row['packageID'];
181 else if ($row['sqlIndex'] === '') {
182 $this->columnPackageIDs
[$row['sqlTable']][$row['sqlColumn']] = $row['packageID'];
184 else if (substr($row['sqlIndex'], -3) === '_fk') {
185 $this->foreignKeyPackageIDs
[$row['sqlTable']][$row['sqlIndex']] = $row['packageID'];
188 $this->indexPackageIDs
[$row['sqlTable']][$row['sqlIndex']] = $row['packageID'];
194 * Adds the given index to the table.
196 * @param string $tableName
197 * @param DatabaseTableForeignKey $foreignKey
199 protected function addForeignKey($tableName, DatabaseTableForeignKey
$foreignKey) {
200 $this->dbEditor
->addForeignKey($tableName, $foreignKey->getName(), $foreignKey->getData());
204 * Adds the given index to the table.
206 * @param string $tableName
207 * @param DatabaseTableIndex $index
209 protected function addIndex($tableName, DatabaseTableIndex
$index) {
210 $this->dbEditor
->addIndex($tableName, $index->getName(), $index->getData());
214 * Applies all of the previously determined changes to achieve the desired database layout.
216 * @throws SplitNodeException if any change has been applied
218 protected function applyChanges() {
219 $appliedAnyChange = false;
221 foreach ($this->tablesToCreate
as $table) {
222 $appliedAnyChange = true;
224 $this->prepareTableLog($table);
225 $this->createTable($table);
226 $this->finalizeTableLog($table);
229 foreach ($this->tablesToDrop
as $table) {
230 $appliedAnyChange = true;
232 $this->dropTable($table);
233 $this->deleteTableLog($table);
236 $columnTables = array_unique(array_merge(
237 array_keys($this->columnsToAdd
),
238 array_keys($this->columnsToAlter
),
239 array_keys($this->columnsToDrop
)
241 foreach ($columnTables as $tableName) {
242 $appliedAnyChange = true;
244 $columnsToAdd = $this->columnsToAdd
[$tableName] ??
[];
245 $columnsToAlter = $this->columnsToAlter
[$tableName] ??
[];
246 $columnsToDrop = $this->columnsToDrop
[$tableName] ??
[];
248 foreach ($columnsToAdd as $column) {
249 $this->prepareColumnLog($tableName, $column);
252 $this->applyColumnChanges(
259 foreach ($columnsToAdd as $column) {
260 $this->finalizeColumnLog($tableName, $column);
263 foreach ($columnsToDrop as $column) {
264 $this->deleteColumnLog($tableName, $column);
268 foreach ($this->foreignKeysToDrop
as $tableName => $foreignKeys) {
269 foreach ($foreignKeys as $foreignKey) {
270 $appliedAnyChange = true;
272 $this->dropForeignKey($tableName, $foreignKey);
273 $this->deleteForeignKeyLog($tableName, $foreignKey);
277 foreach ($this->foreignKeysToAdd
as $tableName => $foreignKeys) {
278 foreach ($foreignKeys as $foreignKey) {
279 $appliedAnyChange = true;
281 $this->prepareForeignKeyLog($tableName, $foreignKey);
282 $this->addForeignKey($tableName, $foreignKey);
283 $this->finalizeForeignKeyLog($tableName, $foreignKey);
287 foreach ($this->indicesToDrop
as $tableName => $indices) {
288 foreach ($indices as $index) {
289 $appliedAnyChange = true;
291 $this->dropIndex($tableName, $index);
292 $this->deleteIndexLog($tableName, $index);
296 foreach ($this->indicesToAdd
as $tableName => $indices) {
297 foreach ($indices as $index) {
298 $appliedAnyChange = true;
300 $this->prepareIndexLog($tableName, $index);
301 $this->addIndex($tableName, $index);
302 $this->finalizeIndexLog($tableName, $index);
306 if ($appliedAnyChange) {
307 throw new SplitNodeException($this->splitNodeMessage
);
312 * Adds, alters, and drop columns of the same table.
314 * Before a column is dropped, all of its foreign keys are dropped.
316 * @param string $tableName
317 * @param IDatabaseTableColumn[] $addedColumns
318 * @param IDatabaseTableColumn[] $alteredColumns
319 * @param IDatabaseTableColumn[] $droppedColumns
321 protected function applyColumnChanges($tableName, array $addedColumns, array $alteredColumns, array $droppedColumns) {
322 $dropForeignKeys = [];
325 foreach ($droppedColumns as $droppedColumn) {
326 $columnData[$droppedColumn->getName()] = [
330 foreach ($this->getExistingTable($tableName)->getForeignKeys() as $foreignKey) {
331 if (in_array($droppedColumn->getName(), $foreignKey->getColumns())) {
332 $dropForeignKeys[] = $foreignKey;
336 foreach ($addedColumns as $addedColumn) {
337 $columnData[$addedColumn->getName()] = [
339 'data' => $addedColumn->getData()
342 foreach ($alteredColumns as $alteredColumn) {
343 $columnData[$alteredColumn->getName()] = [
345 'data' => $alteredColumn->getData(),
346 'oldColumnName' => $alteredColumn->getName()
350 if (!empty($columnData)) {
351 foreach ($dropForeignKeys as $foreignKey) {
352 $this->dropForeignKey($tableName, $foreignKey);
353 $this->deleteForeignKeyLog($tableName, $foreignKey);
356 $this->dbEditor
->alterColumns($tableName, $columnData);
361 * Calculates all of the necessary changes to be executed.
363 protected function calculateChanges() {
364 foreach ($this->tables
as $table) {
365 $tableName = $table->getName();
367 if ($table->willBeDropped()) {
368 if (in_array($tableName, $this->existingTableNames
)) {
369 $this->tablesToDrop
[] = $table;
371 $this->splitNodeMessage
.= "Dropped table '{$tableName}'.";
374 else if (isset($this->tablePackageIDs
[$tableName])) {
375 $this->deleteTableLog($table);
378 else if (!in_array($tableName, $this->existingTableNames
)) {
379 if ($table instanceof PartialDatabaseTable
) {
380 throw new \
LogicException("Partial table '{$tableName}' cannot be created.");
383 $this->tablesToCreate
[] = $table;
385 $this->splitNodeMessage
.= "Created table '{$tableName}'.";
389 // calculate difference between tables
390 $existingTable = $this->getExistingTable($tableName);
391 $existingColumns = $existingTable->getColumns();
393 foreach ($table->getColumns() as $column) {
394 if ($column->willBeDropped()) {
395 if (isset($existingColumns[$column->getName()])) {
396 if (!isset($this->columnsToDrop
[$tableName])) {
397 $this->columnsToDrop
[$tableName] = [];
399 $this->columnsToDrop
[$tableName][] = $column;
401 else if (isset($this->columnPackageIDs
[$tableName][$column->getName()])) {
402 $this->deleteColumnLog($tableName, $column);
405 else if (!isset($existingColumns[$column->getName()])) {
406 if (!isset($this->columnsToAdd
[$tableName])) {
407 $this->columnsToAdd
[$tableName] = [];
409 $this->columnsToAdd
[$tableName][] = $column;
411 else if ($this->diffColumns($existingColumns[$column->getName()], $column)) {
412 if (!isset($this->columnsToAlter
[$tableName])) {
413 $this->columnsToAlter
[$tableName] = [];
415 $this->columnsToAlter
[$tableName][] = $column;
419 // all column-related changes are executed in one query thus break
420 // here and not within the previous loop
421 if (!empty($this->columnsToAdd
) ||
!empty($this->columnsToAlter
) ||
!empty($this->columnsToDrop
)) {
422 $this->splitNodeMessage
.= "Altered columns of table '{$tableName}'.";
426 $existingForeignKeys = $existingTable->getForeignKeys();
427 foreach ($table->getForeignKeys() as $foreignKey) {
428 $matchingExistingForeignKey = null;
429 foreach ($existingForeignKeys as $existingForeignKey) {
430 if (empty(array_diff($foreignKey->getDiffData(), $existingForeignKey->getDiffData()))) {
431 $matchingExistingForeignKey = $existingForeignKey;
436 if ($foreignKey->willBeDropped()) {
437 if ($matchingExistingForeignKey !== null) {
438 if (!isset($this->foreignKeysToDrop
[$tableName])) {
439 $this->foreignKeysToDrop
[$tableName] = [];
441 $this->foreignKeysToDrop
[$tableName][] = $foreignKey;
443 $this->splitNodeMessage
.= "Dropped foreign key '{$tableName}." . implode(',', $foreignKey->getColumns()) . "'.";
446 else if (isset($this->foreignKeyPackageIDs
[$tableName][$foreignKey->getName()])) {
447 $this->deleteForeignKeyLog($tableName, $foreignKey);
450 else if ($matchingExistingForeignKey === null) {
451 // If the referenced database table does not already exists, delay the
452 // foreign key creation until after the referenced table has been created.
453 if (!in_array($foreignKey->getReferencedTable(), $this->existingTableNames
)) {
457 if (!isset($this->foreignKeysToAdd
[$tableName])) {
458 $this->foreignKeysToAdd
[$tableName] = [];
460 $this->foreignKeysToAdd
[$tableName][] = $foreignKey;
462 $this->splitNodeMessage
.= "Added foreign key '{$tableName}." . implode(',', $foreignKey->getColumns()) . "'.";
465 else if (!empty(array_diff($foreignKey->getData(), $matchingExistingForeignKey->getData()))) {
466 if (!isset($this->foreignKeysToDrop
[$tableName])) {
467 $this->foreignKeysToDrop
[$tableName] = [];
469 $this->foreignKeysToDrop
[$tableName][] = $matchingExistingForeignKey;
471 if (!isset($this->foreignKeysToAdd
[$tableName])) {
472 $this->foreignKeysToAdd
[$tableName] = [];
474 $this->foreignKeysToAdd
[$tableName][] = $foreignKey;
476 $this->splitNodeMessage
.= "Replaced foreign key '{$tableName}." . implode(',', $foreignKey->getColumns()) . "'.";
481 $existingIndices = $existingTable->getIndices();
482 foreach ($table->getIndices() as $index) {
483 $matchingExistingIndex = null;
484 foreach ($existingIndices as $existingIndex) {
485 if (!$this->diffIndices($existingIndex, $index)) {
486 $matchingExistingIndex = $existingIndex;
491 if ($index->willBeDropped()) {
492 if ($matchingExistingIndex !== null) {
493 if (!isset($this->indicesToDrop
[$tableName])) {
494 $this->indicesToDrop
[$tableName] = [];
496 $this->indicesToDrop
[$tableName][] = $matchingExistingIndex;
498 $this->splitNodeMessage
.= "Dropped index '{$tableName}." . implode(',', $index->getColumns()) . "'.";
501 else if (isset($this->indexPackageIDs
[$tableName][$index->getName()])) {
502 $this->deleteIndexLog($tableName, $index);
505 else if ($matchingExistingIndex !== null) {
506 // updating index type and index columns is supported with an
507 // explicit index name is given (automatically generated index
508 // names are not deterministic)
509 if (!$index->hasGeneratedName() && !empty(array_diff($matchingExistingIndex->getData(), $index->getData()))) {
510 if (!isset($this->indicesToDrop
[$tableName])) {
511 $this->indicesToDrop
[$tableName] = [];
513 $this->indicesToDrop
[$tableName][] = $matchingExistingIndex;
515 if (!isset($this->indicesToAdd
[$tableName])) {
516 $this->indicesToAdd
[$tableName] = [];
518 $this->indicesToAdd
[$tableName][] = $index;
522 if (!isset($this->indicesToAdd
[$tableName])) {
523 $this->indicesToAdd
[$tableName] = [];
525 $this->indicesToAdd
[$tableName][] = $index;
527 $this->splitNodeMessage
.= "Added index '{$tableName}." . implode(',', $index->getColumns()) . "'.";
536 * Checks for any pending log entries for the package and either marks them as done or
537 * deletes them so that after this method finishes, there are no more undone log entries
540 protected function checkPendingLogEntries() {
542 FROM wcf" . WCF_N
. "_package_installation_sql_log
545 $statement = WCF
::getDB()->prepareStatement($sql);
546 $statement->execute([$this->package
->packageID
, 0]);
548 $doneEntries = $undoneEntries = [];
549 while ($row = $statement->fetchArray()) {
551 if ($row['sqlIndex'] === '' && $row['sqlColumn'] === '') {
552 if (in_array($row['sqlTable'], $this->existingTableNames
)) {
553 $doneEntries[] = $row;
556 $undoneEntries[] = $row;
560 else if ($row['sqlIndex'] === '') {
561 if (isset($this->getExistingTable($row['sqlTable'])->getColumns()[$row['sqlColumn']])) {
562 $doneEntries[] = $row;
565 $undoneEntries[] = $row;
569 else if (substr($row['sqlIndex'], -3) === '_fk') {
570 if (isset($this->getExistingTable($row['sqlTable'])->getForeignKeys()[$row['sqlIndex']])) {
571 $doneEntries[] = $row;
574 $undoneEntries[] = $row;
579 if (isset($this->getExistingTable($row['sqlTable'])->getIndices()[$row['sqlIndex']])) {
580 $doneEntries[] = $row;
583 $undoneEntries[] = $row;
588 WCF
::getDB()->beginTransaction();
589 foreach ($doneEntries as $entry) {
590 $this->finalizeLog($entry);
593 // to achieve a consistent state, undone log entries will be deleted here even though
594 // they might be re-created later to ensure that after this method finishes, there are
595 // no more undone entries in the log for the relevant package
596 foreach ($undoneEntries as $entry) {
597 $this->deleteLog($entry);
599 WCF
::getDB()->commitTransaction();
603 * Creates a done log entry for the given foreign key.
605 * @param string $tableName
606 * @param DatabaseTableForeignKey $foreignKey
608 protected function createForeignKeyLog($tableName, DatabaseTableForeignKey
$foreignKey) {
609 $sql = "INSERT INTO wcf" . WCF_N
. "_package_installation_sql_log
610 (packageID, sqlTable, sqlIndex, isDone)
611 VALUES (?, ?, ?, ?)";
612 $statement = WCF
::getDB()->prepareStatement($sql);
614 $statement->execute([
615 $this->package
->packageID
,
617 $foreignKey->getName(),
623 * Creates the given table.
625 * @param DatabaseTable $table
627 protected function createTable(DatabaseTable
$table) {
628 $hasPrimaryKey = false;
629 $columnData = array_map(function(IDatabaseTableColumn
$column) use (&$hasPrimaryKey) {
630 $data = $column->getData();
631 if (isset($data['key']) && $data['key'] === 'PRIMARY') {
632 $hasPrimaryKey = true;
637 'name' => $column->getName()
639 }, $table->getColumns());
640 $indexData = array_map(function(DatabaseTableIndex
$index) {
642 'data' => $index->getData(),
643 'name' => $index->getName()
645 }, $table->getIndices());
647 // Auto columns are implicitly defined as the primary key by MySQL.
648 if ($hasPrimaryKey) {
649 $indexData = array_filter($indexData, function($key) {
650 return $key !== 'PRIMARY';
651 }, ARRAY_FILTER_USE_KEY
);
654 $this->dbEditor
->createTable($table->getName(), $columnData, $indexData);
656 foreach ($table->getForeignKeys() as $foreignKey) {
657 // Only try to create the foreign key if the referenced database table already exists.
658 // If it will be created later on, delay the foreign key creation until after the
659 // referenced table has been created.
661 in_array($foreignKey->getReferencedTable(), $this->existingTableNames
)
662 ||
$foreignKey->getReferencedTable() === $table->getName()
664 $this->dbEditor
->addForeignKey($table->getName(), $foreignKey->getName(), $foreignKey->getData());
666 // foreign keys need to be explicitly logged for proper uninstallation
667 $this->createForeignKeyLog($table->getName(), $foreignKey);
673 * Deletes the log entry for the given column.
675 * @param string $tableName
676 * @param IDatabaseTableColumn $column
678 protected function deleteColumnLog($tableName, IDatabaseTableColumn
$column) {
679 $this->deleteLog(['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]);
683 * Deletes the log entry for the given foreign key.
685 * @param string $tableName
686 * @param DatabaseTableForeignKey $foreignKey
688 protected function deleteForeignKeyLog($tableName, DatabaseTableForeignKey
$foreignKey) {
689 $this->deleteLog(['sqlTable' => $tableName, 'sqlIndex' => $foreignKey->getName()]);
693 * Deletes the log entry for the given index.
695 * @param string $tableName
696 * @param DatabaseTableIndex $index
698 protected function deleteIndexLog($tableName, DatabaseTableIndex
$index) {
699 $this->deleteLog(['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]);
703 * Deletes a log entry.
707 protected function deleteLog(array $data) {
708 $sql = "DELETE FROM wcf" . WCF_N
. "_package_installation_sql_log
713 $statement = WCF
::getDB()->prepareStatement($sql);
715 $statement->execute([
716 $this->package
->packageID
,
718 $data['sqlColumn'] ??
'',
719 $data['sqlIndex'] ??
''
724 * Deletes all log entry related to the given table.
726 * @param DatabaseTable $table
728 protected function deleteTableLog(DatabaseTable
$table) {
729 $sql = "DELETE FROM wcf" . WCF_N
. "_package_installation_sql_log
732 $statement = WCF
::getDB()->prepareStatement($sql);
734 $statement->execute([
735 $this->package
->packageID
,
741 * Returns `true` if the two columns differ.
743 * @param IDatabaseTableColumn $oldColumn
744 * @param IDatabaseTableColumn $newColumn
747 protected function diffColumns(IDatabaseTableColumn
$oldColumn, IDatabaseTableColumn
$newColumn) {
748 $diff = array_diff($oldColumn->getData(), $newColumn->getData());
750 // see https://github.com/WoltLab/WCF/pull/3167
752 array_key_exists('length', $diff)
753 && $oldColumn instanceof AbstractIntDatabaseTableColumn
755 !($oldColumn instanceof TinyintDatabaseTableColumn
)
756 ||
$oldColumn->getLength() != 1
759 unset($diff['length']);
767 // default type has to be checked explicitly for `null` to properly detect changing
768 // from no default value (`null`) and to an empty string as default value (and vice
770 if ($oldColumn->getDefaultValue() === null ||
$newColumn->getDefaultValue() === null) {
771 return $oldColumn->getDefaultValue() !== $newColumn->getDefaultValue();
774 // for all other cases, use weak comparison so that `'1'` (from database) and `1`
775 // (from script PIP) match, for example
776 return $oldColumn->getDefaultValue() != $newColumn->getDefaultValue();
780 * Returns `true` if the two indices differ.
782 * @param DatabaseTableIndex $oldIndex
783 * @param DatabaseTableIndex $newIndex
786 protected function diffIndices(DatabaseTableIndex
$oldIndex, DatabaseTableIndex
$newIndex) {
787 if ($newIndex->hasGeneratedName()) {
788 return !empty(array_diff($oldIndex->getData(), $newIndex->getData()));
791 return $oldIndex->getName() !== $newIndex->getName();
795 * Drops the given foreign key.
797 * @param string $tableName
798 * @param DatabaseTableForeignKey $foreignKey
800 protected function dropForeignKey($tableName, DatabaseTableForeignKey
$foreignKey) {
801 $this->dbEditor
->dropForeignKey($tableName, $foreignKey->getName());
802 $this->dbEditor
->dropIndex($tableName, $foreignKey->getName());
806 * Drops the given index.
808 * @param string $tableName
809 * @param DatabaseTableIndex $index
811 protected function dropIndex($tableName, DatabaseTableIndex
$index) {
812 $this->dbEditor
->dropIndex($tableName, $index->getName());
816 * Drops the given table.
818 * @param DatabaseTable $table
820 protected function dropTable(DatabaseTable
$table) {
821 $this->dbEditor
->dropTable($table->getName());
825 * Finalizes the log entry for the creation of the given column.
827 * @param string $tableName
828 * @param IDatabaseTableColumn $column
830 protected function finalizeColumnLog($tableName, IDatabaseTableColumn
$column) {
831 $this->finalizeLog(['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]);
835 * Finalizes the log entry for adding the given index.
837 * @param string $tableName
838 * @param DatabaseTableForeignKey $foreignKey
840 protected function finalizeForeignKeyLog($tableName, DatabaseTableForeignKey
$foreignKey) {
841 $this->finalizeLog(['sqlTable' => $tableName, 'sqlIndex' => $foreignKey->getName()]);
845 * Finalizes the log entry for adding the given index.
847 * @param string $tableName
848 * @param DatabaseTableIndex $index
850 protected function finalizeIndexLog($tableName, DatabaseTableIndex
$index) {
851 $this->finalizeLog(['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]);
855 * Finalizes a log entry after the relevant change has been executed.
859 protected function finalizeLog(array $data) {
860 $sql = "UPDATE wcf" . WCF_N
. "_package_installation_sql_log
866 $statement = WCF
::getDB()->prepareStatement($sql);
868 $statement->execute([
870 $this->package
->packageID
,
872 $data['sqlColumn'] ??
'',
873 $data['sqlIndex'] ??
''
878 * Finalizes the log entry for the creation of the given table.
880 * @param DatabaseTable $table
882 protected function finalizeTableLog(DatabaseTable
$table) {
883 $this->finalizeLog(['sqlTable' => $table->getName()]);
887 * Returns the id of the package to with the given column belongs to. If there is no specific
888 * log entry for the given column, the table log is checked and the relevant package id of
889 * the whole table is returned. If the package of the table is also unknown, `null` is returned.
891 * @param DatabaseTable $table
892 * @param IDatabaseTableColumn $column
895 protected function getColumnPackageID(DatabaseTable
$table, IDatabaseTableColumn
$column) {
896 if (isset($this->columnPackageIDs
[$table->getName()][$column->getName()])) {
897 return $this->columnPackageIDs
[$table->getName()][$column->getName()];
899 else if (isset($this->tablePackageIDs
[$table->getName()])) {
900 return $this->tablePackageIDs
[$table->getName()];
907 * Returns the `DatabaseTable` object for the table with the given name.
909 * @param string $tableName
910 * @return DatabaseTable
912 protected function getExistingTable($tableName) {
913 if (!isset($this->existingTables
[$tableName])) {
914 $this->existingTables
[$tableName] = DatabaseTable
::createFromExistingTable($this->dbEditor
, $tableName);
917 return $this->existingTables
[$tableName];
921 * Returns the id of the package to with the given foreign key belongs to. If there is no specific
922 * log entry for the given foreign key, the table log is checked and the relevant package id of
923 * the whole table is returned. If the package of the table is also unknown, `null` is returned.
925 * @param DatabaseTable $table
926 * @param DatabaseTableForeignKey $foreignKey
929 protected function getForeignKeyPackageID(DatabaseTable
$table, DatabaseTableForeignKey
$foreignKey) {
930 if (isset($this->foreignKeyPackageIDs
[$table->getName()][$foreignKey->getName()])) {
931 return $this->foreignKeyPackageIDs
[$table->getName()][$foreignKey->getName()];
933 else if (isset($this->tablePackageIDs
[$table->getName()])) {
934 return $this->tablePackageIDs
[$table->getName()];
941 * Returns the id of the package to with the given index belongs to. If there is no specific
942 * log entry for the given index, the table log is checked and the relevant package id of
943 * the whole table is returned. If the package of the table is also unknown, `null` is returned.
945 * @param DatabaseTable $table
946 * @param DatabaseTableIndex $index
949 protected function getIndexPackageID(DatabaseTable
$table, DatabaseTableIndex
$index) {
950 if (isset($this->indexPackageIDs
[$table->getName()][$index->getName()])) {
951 return $this->indexPackageIDs
[$table->getName()][$index->getName()];
953 else if (isset($this->tablePackageIDs
[$table->getName()])) {
954 return $this->tablePackageIDs
[$table->getName()];
961 * Prepares the log entry for the creation of the given column.
963 * @param string $tableName
964 * @param IDatabaseTableColumn $column
966 protected function prepareColumnLog($tableName, IDatabaseTableColumn
$column) {
967 $this->prepareLog(['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]);
971 * Prepares the log entry for adding the given foreign key.
973 * @param string $tableName
974 * @param DatabaseTableForeignKey $foreignKey
976 protected function prepareForeignKeyLog($tableName, DatabaseTableForeignKey
$foreignKey) {
977 $this->prepareLog(['sqlTable' => $tableName, 'sqlIndex' => $foreignKey->getName()]);
981 * Prepares the log entry for adding the given index.
983 * @param string $tableName
984 * @param DatabaseTableIndex $index
986 protected function prepareIndexLog($tableName, DatabaseTableIndex
$index) {
987 $this->prepareLog(['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]);
991 * Prepares a log entry before the relevant change has been executed.
995 protected function prepareLog(array $data) {
996 $sql = "INSERT INTO wcf" . WCF_N
. "_package_installation_sql_log
997 (packageID, sqlTable, sqlColumn, sqlIndex, isDone)
998 VALUES (?, ?, ?, ?, ?)";
999 $statement = WCF
::getDB()->prepareStatement($sql);
1001 $statement->execute([
1002 $this->package
->packageID
,
1004 $data['sqlColumn'] ??
'',
1005 $data['sqlIndex'] ??
'',
1011 * Prepares the log entry for the creation of the given table.
1013 * @param DatabaseTable $table
1015 protected function prepareTableLog(DatabaseTable
$table) {
1016 $this->prepareLog(['sqlTable' => $table->getName()]);
1020 * Processes all tables and updates the current table layouts to match the specified layouts.
1022 * @throws \RuntimeException if validation of the required layout changes fails
1024 public function process() {
1025 $this->checkPendingLogEntries();
1027 $errors = $this->validate();
1028 if (!empty($errors)) {
1029 throw new \
RuntimeException(WCF
::getLanguage()->getDynamicVariable('wcf.acp.package.error.databaseChange', [
1034 $this->calculateChanges();
1036 $this->applyChanges();
1040 * Checks if the relevant table layout changes can be executed and returns an array with information
1041 * on all validation errors.
1045 public function validate() {
1047 foreach ($this->tables
as $table) {
1048 if ($table->willBeDropped()) {
1049 if (in_array($table->getName(), $this->existingTableNames
)) {
1050 if (!isset($this->tablePackageIDs
[$table->getName()])) {
1052 'tableName' => $table->getName(),
1053 'type' => 'unregisteredTableDrop'
1056 else if ($this->tablePackageIDs
[$table->getName()] !== $this->package
->packageID
) {
1058 'tableName' => $table->getName(),
1059 'type' => 'foreignTableDrop'
1065 $existingTable = null;
1066 if (in_array($table->getName(), $this->existingTableNames
)) {
1067 if (!isset($this->tablePackageIDs
[$table->getName()])) {
1069 'tableName' => $table->getName(),
1070 'type' => 'unregisteredTableChange',
1074 $existingTable = DatabaseTable
::createFromExistingTable($this->dbEditor
, $table->getName());
1075 $existingColumns = $existingTable->getColumns();
1076 $existingIndices = $existingTable->getIndices();
1077 $existingForeignKeys = $existingTable->getForeignKeys();
1079 foreach ($table->getColumns() as $column) {
1080 if (isset($existingColumns[$column->getName()])) {
1081 $columnPackageID = $this->getColumnPackageID($table, $column);
1082 if ($column->willBeDropped()) {
1083 if ($columnPackageID !== $this->package
->packageID
) {
1085 'columnName' => $column->getName(),
1086 'tableName' => $table->getName(),
1087 'type' => 'foreignColumnDrop',
1091 else if ($columnPackageID !== $this->package
->packageID
) {
1093 'columnName' => $column->getName(),
1094 'tableName' => $table->getName(),
1095 'type' => 'foreignColumnChange',
1101 foreach ($table->getIndices() as $index) {
1102 foreach ($existingIndices as $existingIndex) {
1103 if (empty(array_diff($index->getData(), $existingIndex->getData()))) {
1104 if ($index->willBeDropped()) {
1105 if ($this->getIndexPackageID($table, $index) !== $this->package
->packageID
) {
1107 'columnNames' => implode(',', $existingIndex->getColumns()),
1108 'tableName' => $table->getName(),
1109 'type' => 'foreignIndexDrop',
1119 foreach ($table->getForeignKeys() as $foreignKey) {
1120 foreach ($existingForeignKeys as $existingForeignKey) {
1121 if (empty(array_diff($foreignKey->getData(), $existingForeignKey->getData()))) {
1122 if ($foreignKey->willBeDropped()) {
1123 if ($this->getForeignKeyPackageID($table, $foreignKey) !== $this->package
->packageID
) {
1125 'columnNames' => implode(',', $existingForeignKey->getColumns()),
1126 'tableName' => $table->getName(),
1127 'type' => 'foreignForeignKeyDrop',
1139 foreach ($table->getIndices() as $index) {
1140 foreach ($index->getColumns() as $indexColumn) {
1141 $column = $this->getColumnByName($indexColumn, $table, $existingTable);
1142 if ($column === null) {
1143 if (!$index->willBeDropped()) {
1145 'columnName' => $indexColumn,
1146 'columnNames' => implode(',', $index->getColumns()),
1147 'tableName' => $table->getName(),
1148 'type' => 'nonexistingColumnInIndex',
1153 $index->getType() === DatabaseTableIndex
::PRIMARY_TYPE
1154 && !$index->willBeDropped()
1155 && !$column->isNotNull()
1158 'columnName' => $indexColumn,
1159 'columnNames' => implode(',', $index->getColumns()),
1160 'tableName' => $table->getName(),
1161 'type' => 'nullColumnInPrimaryIndex',
1167 foreach ($table->getForeignKeys() as $foreignKey) {
1168 $referencedTableExists = in_array($foreignKey->getReferencedTable(), $this->existingTableNames
);
1169 foreach ($this->tables
as $processedTable) {
1170 if ($processedTable->getName() === $foreignKey->getReferencedTable()) {
1171 $referencedTableExists = !$processedTable->willBeDropped();
1175 if (!$referencedTableExists) {
1177 'columnNames' => implode(',', $foreignKey->getColumns()),
1178 'referencedTableName' => $foreignKey->getReferencedTable(),
1179 'tableName' => $table->getName(),
1180 'type' => 'unknownTableInForeignKey',
1191 * Returns the column with the given name from the given table.
1193 * @param string $columnName
1194 * @param DatabaseTable $updateTable
1195 * @param DatabaseTable|null $existingTable
1196 * @return IDatabaseTableColumn|null
1199 protected function getColumnByName($columnName, DatabaseTable
$updateTable, DatabaseTable
$existingTable = null) {
1200 foreach ($updateTable->getColumns() as $column) {
1201 if ($column->getName() === $columnName) {
1206 if ($existingTable) {
1207 foreach ($existingTable->getColumns() as $column) {
1208 if ($column->getName() === $columnName) {