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 (!isset($this->foreignKeysToAdd
[$tableName])) {
452 $this->foreignKeysToAdd
[$tableName] = [];
454 $this->foreignKeysToAdd
[$tableName][] = $foreignKey;
456 $this->splitNodeMessage
.= "Added foreign key '{$tableName}." . implode(',', $foreignKey->getColumns()) . "'.";
459 else if (!empty(array_diff($foreignKey->getData(), $matchingExistingForeignKey->getData()))) {
460 if (!isset($this->foreignKeysToDrop
[$tableName])) {
461 $this->foreignKeysToDrop
[$tableName] = [];
463 $this->foreignKeysToDrop
[$tableName][] = $matchingExistingForeignKey;
465 if (!isset($this->foreignKeysToAdd
[$tableName])) {
466 $this->foreignKeysToAdd
[$tableName] = [];
468 $this->foreignKeysToAdd
[$tableName][] = $foreignKey;
470 $this->splitNodeMessage
.= "Replaced foreign key '{$tableName}." . implode(',', $foreignKey->getColumns()) . "'.";
475 $existingIndices = $existingTable->getIndices();
476 foreach ($table->getIndices() as $index) {
477 $matchingExistingIndex = null;
478 foreach ($existingIndices as $existingIndex) {
479 if (!$this->diffIndices($existingIndex, $index)) {
480 $matchingExistingIndex = $existingIndex;
485 if ($index->willBeDropped()) {
486 if ($matchingExistingIndex !== null) {
487 if (!isset($this->indicesToDrop
[$tableName])) {
488 $this->indicesToDrop
[$tableName] = [];
490 $this->indicesToDrop
[$tableName][] = $matchingExistingIndex;
492 $this->splitNodeMessage
.= "Dropped index '{$tableName}." . implode(',', $index->getColumns()) . "'.";
495 else if (isset($this->indexPackageIDs
[$tableName][$index->getName()])) {
496 $this->deleteIndexLog($tableName, $index);
499 else if ($matchingExistingIndex !== null) {
500 // updating index type and index columns is supported with an
501 // explicit index name is given (automatically generated index
502 // names are not deterministic)
503 if (!$index->hasGeneratedName() && !empty(array_diff($matchingExistingIndex->getData(), $index->getData()))) {
504 if (!isset($this->indicesToDrop
[$tableName])) {
505 $this->indicesToDrop
[$tableName] = [];
507 $this->indicesToDrop
[$tableName][] = $matchingExistingIndex;
509 if (!isset($this->indicesToAdd
[$tableName])) {
510 $this->indicesToAdd
[$tableName] = [];
512 $this->indicesToAdd
[$tableName][] = $index;
516 if (!isset($this->indicesToAdd
[$tableName])) {
517 $this->indicesToAdd
[$tableName] = [];
519 $this->indicesToAdd
[$tableName][] = $index;
521 $this->splitNodeMessage
.= "Added index '{$tableName}." . implode(',', $index->getColumns()) . "'.";
530 * Checks for any pending log entries for the package and either marks them as done or
531 * deletes them so that after this method finishes, there are no more undone log entries
534 protected function checkPendingLogEntries() {
536 FROM wcf" . WCF_N
. "_package_installation_sql_log
539 $statement = WCF
::getDB()->prepareStatement($sql);
540 $statement->execute([$this->package
->packageID
, 0]);
542 $doneEntries = $undoneEntries = [];
543 while ($row = $statement->fetchArray()) {
545 if ($row['sqlIndex'] === '' && $row['sqlColumn'] === '') {
546 if (in_array($row['sqlTable'], $this->existingTableNames
)) {
547 $doneEntries[] = $row;
550 $undoneEntries[] = $row;
554 else if ($row['sqlIndex'] === '') {
555 if (isset($this->getExistingTable($row['sqlTable'])->getColumns()[$row['sqlColumn']])) {
556 $doneEntries[] = $row;
559 $undoneEntries[] = $row;
563 else if (substr($row['sqlIndex'], -3) === '_fk') {
564 if (isset($this->getExistingTable($row['sqlTable'])->getForeignKeys()[$row['sqlIndex']])) {
565 $doneEntries[] = $row;
568 $undoneEntries[] = $row;
573 if (isset($this->getExistingTable($row['sqlTable'])->getIndices()[$row['sqlIndex']])) {
574 $doneEntries[] = $row;
577 $undoneEntries[] = $row;
582 WCF
::getDB()->beginTransaction();
583 foreach ($doneEntries as $entry) {
584 $this->finalizeLog($entry);
587 // to achieve a consistent state, undone log entries will be deleted here even though
588 // they might be re-created later to ensure that after this method finishes, there are
589 // no more undone entries in the log for the relevant package
590 foreach ($undoneEntries as $entry) {
591 $this->deleteLog($entry);
593 WCF
::getDB()->commitTransaction();
597 * Creates a done log entry for the given foreign key.
599 * @param string $tableName
600 * @param DatabaseTableForeignKey $foreignKey
602 protected function createForeignKeyLog($tableName, DatabaseTableForeignKey
$foreignKey) {
603 $sql = "INSERT INTO wcf" . WCF_N
. "_package_installation_sql_log
604 (packageID, sqlTable, sqlIndex, isDone)
605 VALUES (?, ?, ?, ?)";
606 $statement = WCF
::getDB()->prepareStatement($sql);
608 $statement->execute([
609 $this->package
->packageID
,
611 $foreignKey->getName(),
617 * Creates the given table.
619 * @param DatabaseTable $table
621 protected function createTable(DatabaseTable
$table) {
622 $hasPrimaryKey = false;
623 $columnData = array_map(function(IDatabaseTableColumn
$column) use (&$hasPrimaryKey) {
624 $data = $column->getData();
625 if (isset($data['key']) && $data['key'] === 'PRIMARY') {
626 $hasPrimaryKey = true;
631 'name' => $column->getName()
633 }, $table->getColumns());
634 $indexData = array_map(function(DatabaseTableIndex
$index) {
636 'data' => $index->getData(),
637 'name' => $index->getName()
639 }, $table->getIndices());
641 // Auto columns are implicitly defined as the primary key by MySQL.
642 if ($hasPrimaryKey) {
643 $indexData = array_filter($indexData, function($key) {
644 return $key !== 'PRIMARY';
645 }, ARRAY_FILTER_USE_KEY
);
648 $this->dbEditor
->createTable($table->getName(), $columnData, $indexData);
650 foreach ($table->getForeignKeys() as $foreignKey) {
651 $this->dbEditor
->addForeignKey($table->getName(), $foreignKey->getName(), $foreignKey->getData());
653 // foreign keys need to be explicitly logged for proper uninstallation
654 $this->createForeignKeyLog($table->getName(), $foreignKey);
659 * Deletes the log entry for the given column.
661 * @param string $tableName
662 * @param IDatabaseTableColumn $column
664 protected function deleteColumnLog($tableName, IDatabaseTableColumn
$column) {
665 $this->deleteLog(['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]);
669 * Deletes the log entry for the given foreign key.
671 * @param string $tableName
672 * @param DatabaseTableForeignKey $foreignKey
674 protected function deleteForeignKeyLog($tableName, DatabaseTableForeignKey
$foreignKey) {
675 $this->deleteLog(['sqlTable' => $tableName, 'sqlIndex' => $foreignKey->getName()]);
679 * Deletes the log entry for the given index.
681 * @param string $tableName
682 * @param DatabaseTableIndex $index
684 protected function deleteIndexLog($tableName, DatabaseTableIndex
$index) {
685 $this->deleteLog(['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]);
689 * Deletes a log entry.
693 protected function deleteLog(array $data) {
694 $sql = "DELETE FROM wcf" . WCF_N
. "_package_installation_sql_log
699 $statement = WCF
::getDB()->prepareStatement($sql);
701 $statement->execute([
702 $this->package
->packageID
,
704 $data['sqlColumn'] ??
'',
705 $data['sqlIndex'] ??
''
710 * Deletes all log entry related to the given table.
712 * @param DatabaseTable $table
714 protected function deleteTableLog(DatabaseTable
$table) {
715 $sql = "DELETE FROM wcf" . WCF_N
. "_package_installation_sql_log
718 $statement = WCF
::getDB()->prepareStatement($sql);
720 $statement->execute([
721 $this->package
->packageID
,
727 * Returns `true` if the two columns differ.
729 * @param IDatabaseTableColumn $oldColumn
730 * @param IDatabaseTableColumn $newColumn
733 protected function diffColumns(IDatabaseTableColumn
$oldColumn, IDatabaseTableColumn
$newColumn) {
734 $diff = array_diff($oldColumn->getData(), $newColumn->getData());
736 // see https://github.com/WoltLab/WCF/pull/3167
738 array_key_exists('length', $diff)
739 && $oldColumn instanceof AbstractIntDatabaseTableColumn
741 !($oldColumn instanceof TinyintDatabaseTableColumn
)
742 ||
$oldColumn->getLength() != 1
745 unset($diff['length']);
753 // default type has to be checked explicitly for `null` to properly detect changing
754 // from no default value (`null`) and to an empty string as default value (and vice
756 if ($oldColumn->getDefaultValue() === null ||
$newColumn->getDefaultValue() === null) {
757 return $oldColumn->getDefaultValue() !== $newColumn->getDefaultValue();
760 // for all other cases, use weak comparison so that `'1'` (from database) and `1`
761 // (from script PIP) match, for example
762 return $oldColumn->getDefaultValue() != $newColumn->getDefaultValue();
766 * Returns `true` if the two indices differ.
768 * @param DatabaseTableIndex $oldIndex
769 * @param DatabaseTableIndex $newIndex
772 protected function diffIndices(DatabaseTableIndex
$oldIndex, DatabaseTableIndex
$newIndex) {
773 if ($newIndex->hasGeneratedName()) {
774 return !empty(array_diff($oldIndex->getData(), $newIndex->getData()));
777 return $oldIndex->getName() !== $newIndex->getName();
781 * Drops the given foreign key.
783 * @param string $tableName
784 * @param DatabaseTableForeignKey $foreignKey
786 protected function dropForeignKey($tableName, DatabaseTableForeignKey
$foreignKey) {
787 $this->dbEditor
->dropForeignKey($tableName, $foreignKey->getName());
788 $this->dbEditor
->dropIndex($tableName, $foreignKey->getName());
792 * Drops the given index.
794 * @param string $tableName
795 * @param DatabaseTableIndex $index
797 protected function dropIndex($tableName, DatabaseTableIndex
$index) {
798 $this->dbEditor
->dropIndex($tableName, $index->getName());
802 * Drops the given table.
804 * @param DatabaseTable $table
806 protected function dropTable(DatabaseTable
$table) {
807 $this->dbEditor
->dropTable($table->getName());
811 * Finalizes the log entry for the creation of the given column.
813 * @param string $tableName
814 * @param IDatabaseTableColumn $column
816 protected function finalizeColumnLog($tableName, IDatabaseTableColumn
$column) {
817 $this->finalizeLog(['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]);
821 * Finalizes the log entry for adding the given index.
823 * @param string $tableName
824 * @param DatabaseTableForeignKey $foreignKey
826 protected function finalizeForeignKeyLog($tableName, DatabaseTableForeignKey
$foreignKey) {
827 $this->finalizeLog(['sqlTable' => $tableName, 'sqlIndex' => $foreignKey->getName()]);
831 * Finalizes the log entry for adding the given index.
833 * @param string $tableName
834 * @param DatabaseTableIndex $index
836 protected function finalizeIndexLog($tableName, DatabaseTableIndex
$index) {
837 $this->finalizeLog(['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]);
841 * Finalizes a log entry after the relevant change has been executed.
845 protected function finalizeLog(array $data) {
846 $sql = "UPDATE wcf" . WCF_N
. "_package_installation_sql_log
852 $statement = WCF
::getDB()->prepareStatement($sql);
854 $statement->execute([
856 $this->package
->packageID
,
858 $data['sqlColumn'] ??
'',
859 $data['sqlIndex'] ??
''
864 * Finalizes the log entry for the creation of the given table.
866 * @param DatabaseTable $table
868 protected function finalizeTableLog(DatabaseTable
$table) {
869 $this->finalizeLog(['sqlTable' => $table->getName()]);
873 * Returns the id of the package to with the given column belongs to. If there is no specific
874 * log entry for the given column, the table log is checked and the relevant package id of
875 * the whole table is returned. If the package of the table is also unknown, `null` is returned.
877 * @param DatabaseTable $table
878 * @param IDatabaseTableColumn $column
881 protected function getColumnPackageID(DatabaseTable
$table, IDatabaseTableColumn
$column) {
882 if (isset($this->columnPackageIDs
[$table->getName()][$column->getName()])) {
883 return $this->columnPackageIDs
[$table->getName()][$column->getName()];
885 else if (isset($this->tablePackageIDs
[$table->getName()])) {
886 return $this->tablePackageIDs
[$table->getName()];
893 * Returns the `DatabaseTable` object for the table with the given name.
895 * @param string $tableName
896 * @return DatabaseTable
898 protected function getExistingTable($tableName) {
899 if (!isset($this->existingTables
[$tableName])) {
900 $this->existingTables
[$tableName] = DatabaseTable
::createFromExistingTable($this->dbEditor
, $tableName);
903 return $this->existingTables
[$tableName];
907 * Returns the id of the package to with the given foreign key belongs to. If there is no specific
908 * log entry for the given foreign key, the table log is checked and the relevant package id of
909 * the whole table is returned. If the package of the table is also unknown, `null` is returned.
911 * @param DatabaseTable $table
912 * @param DatabaseTableForeignKey $foreignKey
915 protected function getForeignKeyPackageID(DatabaseTable
$table, DatabaseTableForeignKey
$foreignKey) {
916 if (isset($this->foreignKeyPackageIDs
[$table->getName()][$foreignKey->getName()])) {
917 return $this->foreignKeyPackageIDs
[$table->getName()][$foreignKey->getName()];
919 else if (isset($this->tablePackageIDs
[$table->getName()])) {
920 return $this->tablePackageIDs
[$table->getName()];
927 * Returns the id of the package to with the given index belongs to. If there is no specific
928 * log entry for the given index, the table log is checked and the relevant package id of
929 * the whole table is returned. If the package of the table is also unknown, `null` is returned.
931 * @param DatabaseTable $table
932 * @param DatabaseTableIndex $index
935 protected function getIndexPackageID(DatabaseTable
$table, DatabaseTableIndex
$index) {
936 if (isset($this->indexPackageIDs
[$table->getName()][$index->getName()])) {
937 return $this->indexPackageIDs
[$table->getName()][$index->getName()];
939 else if (isset($this->tablePackageIDs
[$table->getName()])) {
940 return $this->tablePackageIDs
[$table->getName()];
947 * Prepares the log entry for the creation of the given column.
949 * @param string $tableName
950 * @param IDatabaseTableColumn $column
952 protected function prepareColumnLog($tableName, IDatabaseTableColumn
$column) {
953 $this->prepareLog(['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]);
957 * Prepares the log entry for adding the given foreign key.
959 * @param string $tableName
960 * @param DatabaseTableForeignKey $foreignKey
962 protected function prepareForeignKeyLog($tableName, DatabaseTableForeignKey
$foreignKey) {
963 $this->prepareLog(['sqlTable' => $tableName, 'sqlIndex' => $foreignKey->getName()]);
967 * Prepares the log entry for adding the given index.
969 * @param string $tableName
970 * @param DatabaseTableIndex $index
972 protected function prepareIndexLog($tableName, DatabaseTableIndex
$index) {
973 $this->prepareLog(['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]);
977 * Prepares a log entry before the relevant change has been executed.
981 protected function prepareLog(array $data) {
982 $sql = "INSERT INTO wcf" . WCF_N
. "_package_installation_sql_log
983 (packageID, sqlTable, sqlColumn, sqlIndex, isDone)
984 VALUES (?, ?, ?, ?, ?)";
985 $statement = WCF
::getDB()->prepareStatement($sql);
987 $statement->execute([
988 $this->package
->packageID
,
990 $data['sqlColumn'] ??
'',
991 $data['sqlIndex'] ??
'',
997 * Prepares the log entry for the creation of the given table.
999 * @param DatabaseTable $table
1001 protected function prepareTableLog(DatabaseTable
$table) {
1002 $this->prepareLog(['sqlTable' => $table->getName()]);
1006 * Processes all tables and updates the current table layouts to match the specified layouts.
1008 * @throws \RuntimeException if validation of the required layout changes fails
1010 public function process() {
1011 $this->checkPendingLogEntries();
1013 $errors = $this->validate();
1014 if (!empty($errors)) {
1015 throw new \
RuntimeException(WCF
::getLanguage()->getDynamicVariable('wcf.acp.package.error.databaseChange', [
1020 $this->calculateChanges();
1022 $this->applyChanges();
1026 * Checks if the relevant table layout changes can be executed and returns an array with information
1027 * on all validation errors.
1031 public function validate() {
1033 foreach ($this->tables
as $table) {
1034 if ($table->willBeDropped()) {
1035 if (in_array($table->getName(), $this->existingTableNames
)) {
1036 if (!isset($this->tablePackageIDs
[$table->getName()])) {
1038 'tableName' => $table->getName(),
1039 'type' => 'unregisteredTableDrop'
1042 else if ($this->tablePackageIDs
[$table->getName()] !== $this->package
->packageID
) {
1044 'tableName' => $table->getName(),
1045 'type' => 'foreignTableDrop'
1051 $existingTable = null;
1052 if (in_array($table->getName(), $this->existingTableNames
)) {
1053 if (!isset($this->tablePackageIDs
[$table->getName()])) {
1055 'tableName' => $table->getName(),
1056 'type' => 'unregisteredTableChange',
1060 $existingTable = DatabaseTable
::createFromExistingTable($this->dbEditor
, $table->getName());
1061 $existingColumns = $existingTable->getColumns();
1062 $existingIndices = $existingTable->getIndices();
1063 $existingForeignKeys = $existingTable->getForeignKeys();
1065 foreach ($table->getColumns() as $column) {
1066 if (isset($existingColumns[$column->getName()])) {
1067 $columnPackageID = $this->getColumnPackageID($table, $column);
1068 if ($column->willBeDropped()) {
1069 if ($columnPackageID !== $this->package
->packageID
) {
1071 'columnName' => $column->getName(),
1072 'tableName' => $table->getName(),
1073 'type' => 'foreignColumnDrop',
1077 else if ($columnPackageID !== $this->package
->packageID
) {
1079 'columnName' => $column->getName(),
1080 'tableName' => $table->getName(),
1081 'type' => 'foreignColumnChange',
1087 foreach ($table->getIndices() as $index) {
1088 foreach ($existingIndices as $existingIndex) {
1089 if (empty(array_diff($index->getData(), $existingIndex->getData()))) {
1090 if ($index->willBeDropped()) {
1091 if ($this->getIndexPackageID($table, $index) !== $this->package
->packageID
) {
1093 'columnNames' => implode(',', $existingIndex->getColumns()),
1094 'tableName' => $table->getName(),
1095 'type' => 'foreignIndexDrop',
1105 foreach ($table->getForeignKeys() as $foreignKey) {
1106 foreach ($existingForeignKeys as $existingForeignKey) {
1107 if (empty(array_diff($foreignKey->getData(), $existingForeignKey->getData()))) {
1108 if ($foreignKey->willBeDropped()) {
1109 if ($this->getForeignKeyPackageID($table, $foreignKey) !== $this->package
->packageID
) {
1111 'columnNames' => implode(',', $existingForeignKey->getColumns()),
1112 'tableName' => $table->getName(),
1113 'type' => 'foreignForeignKeyDrop',
1125 foreach ($table->getIndices() as $index) {
1126 foreach ($index->getColumns() as $indexColumn) {
1127 $column = $this->getColumnByName($indexColumn, $table, $existingTable);
1128 if ($column === null) {
1129 if (!$index->willBeDropped()) {
1131 'columnName' => $indexColumn,
1132 'columnNames' => implode(',', $index->getColumns()),
1133 'tableName' => $table->getName(),
1134 'type' => 'nonexistingColumnInIndex',
1139 $index->getType() === DatabaseTableIndex
::PRIMARY_TYPE
1140 && !$index->willBeDropped()
1141 && !$column->isNotNull()
1144 'columnName' => $indexColumn,
1145 'columnNames' => implode(',', $index->getColumns()),
1146 'tableName' => $table->getName(),
1147 'type' => 'nullColumnInPrimaryIndex',
1153 foreach ($table->getForeignKeys() as $foreignKey) {
1154 $referencedTableExists = in_array($foreignKey->getReferencedTable(), $this->existingTableNames
);
1155 foreach ($this->tables
as $processedTable) {
1156 if ($processedTable->getName() === $foreignKey->getReferencedTable()) {
1157 $referencedTableExists = !$processedTable->willBeDropped();
1161 if (!$referencedTableExists) {
1163 'columnNames' => implode(',', $foreignKey->getColumns()),
1164 'referencedTableName' => $foreignKey->getReferencedTable(),
1165 'tableName' => $table->getName(),
1166 'type' => 'unknownTableInForeignKey',
1177 * Returns the column with the given name from the given table.
1179 * @param string $columnName
1180 * @param DatabaseTable $updateTable
1181 * @param DatabaseTable|null $existingTable
1182 * @return IDatabaseTableColumn|null
1185 protected function getColumnByName($columnName, DatabaseTable
$updateTable, DatabaseTable
$existingTable = null) {
1186 foreach ($updateTable->getColumns() as $column) {
1187 if ($column->getName() === $columnName) {
1192 if ($existingTable) {
1193 foreach ($existingTable->getColumns() as $column) {
1194 if ($column->getName() === $columnName) {