From: Matthias Schmidt Date: Sun, 8 Sep 2019 13:11:11 +0000 (+0200) Subject: Update logging procedure of database change X-Git-Tag: 5.2.0_Beta_2~29^2~5 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=3de2e19175c67853d6005b0f5d80db4feb3f748f;p=GitHub%2FWoltLab%2FWCF.git Update logging procedure of database change 1. Create a log entry marked as not done yet 2. Execute the change 3. Mark the log entry as done (When deleting a component, no log entry is created and the relevant log entry is deleted in the end.) --- diff --git a/wcfsetup/install/files/lib/system/database/table/DatabaseTableChangeProcessor.class.php b/wcfsetup/install/files/lib/system/database/table/DatabaseTableChangeProcessor.class.php index 7fe862cb16..6b8d37c3b5 100644 --- a/wcfsetup/install/files/lib/system/database/table/DatabaseTableChangeProcessor.class.php +++ b/wcfsetup/install/files/lib/system/database/table/DatabaseTableChangeProcessor.class.php @@ -20,64 +20,67 @@ use wcf\system\WCF; */ class DatabaseTableChangeProcessor { /** - * added columns grouped by the table they belong to - * @var IDatabaseTableColumn[][] + * maps the registered database table column names to the ids of the packages they belong to + * @var int[][] */ - protected $addedColumns = []; + protected $columnPackageIDs = []; /** - * added indices grouped by the table they belong to - * @var DatabaseTableIndex[][] + * database table columns that will be added grouped by the name of the table to which they + * will be added + * @var IDatabaseTableColumn[][] */ - protected $addedIndices = []; + protected $columnsToAdd = []; /** - * added tables - * @var DatabaseTable[] + * database table columns that will be altered grouped by the name of the table to which + * they belong + * @var IDatabaseTableColumn[][] */ - protected $addedTables = []; + protected $columnsToAlter = []; /** - * maps the registered database table column names to the ids of the packages they belong to - * @var int[][] + * database table columns that will be dropped grouped by the name of the table from which + * they will be dropped + * @var IDatabaseTableColumn[][] */ - protected $columnPackageIDs = []; + protected $columnsToDrop = []; /** * database editor to apply the relevant changes to the table layouts * @var DatabaseEditor */ protected $dbEditor; - + /** - * dropped columns grouped by the table they belong to - * @var IDatabaseTableColumn[][] + * list of all existing tables in the used database + * @var string[] */ - protected $droppedColumns = []; + protected $existingTableNames = []; /** - * dropped indices grouped by the table they belong to - * @var DatabaseTableIndex[][]|DatabaseTableForeignKey[][] + * existing database tables + * @var DatabaseTable[] */ - protected $droppedIndices = []; + protected $existingTables = []; /** - * dropped tables - * @var DatabaseTable[] + * maps the registered database table index names to the ids of the packages they belong to + * @var int[][] */ - protected $droppedTables = []; + protected $indexPackageIDs = []; /** - * list of all existing tables in the used database - * @var string[] + * indices that will be added grouped by the name of the table to which they will be added + * @var DatabaseTableIndex[][] */ - protected $existingTableNames = []; + protected $indicesToAdd = []; /** - * maps the registered database table index names to the ids of the packages they belong to - * @var int[][] + * indices that will be dropped grouped by the name of the table from which they will be dropped + * @var DatabaseTableIndex[][] */ - protected $indexPackageIDs = []; + protected $indicesToDrop = []; /** * maps the registered database table foreign key names to the ids of the packages they belong to @@ -85,6 +88,20 @@ class DatabaseTableChangeProcessor { */ protected $foreignKeyPackageIDs = []; + /** + * foreign keys that will be added grouped by the name of the table to which they will be + * added + * @var DatabaseTableForeignKey[][] + */ + protected $foreignKeysToAdd = []; + + /** + * foreign keys that will be dropped grouped by the name of the table from which they will + * be dropped + * @var DatabaseTableForeignKey[][] + */ + protected $foreignKeysToDrop = []; + /** * is `true` if only one change will be handled per request * @var bool @@ -97,6 +114,12 @@ class DatabaseTableChangeProcessor { */ protected $package; + /** + * message for the split node exception thrown after the changes have been applied + * @var string + */ + protected $splitNodeMessage = ''; + /** * layouts/layout changes of the relevant database table * @var DatabaseTable[] @@ -109,6 +132,18 @@ class DatabaseTableChangeProcessor { */ protected $tablePackageIDs = []; + /** + * database table that will be created + * @var DatabaseTable[] + */ + protected $tablesToCreate = []; + + /** + * database tables that will be dropped + * @var DatabaseTable[] + */ + protected $tablesToDrop = []; + /** * Creates a new instance of `DatabaseTableChangeProcessor`. * @@ -137,9 +172,10 @@ class DatabaseTableChangeProcessor { $conditionBuilder = new PreparedStatementConditionBuilder(); $conditionBuilder->add('sqlTable IN (?)', [$tableNames]); + $conditionBuilder->add('isDone = ?', [1]); $sql = "SELECT * - FROM wcf".WCF_N."_package_installation_sql_log + FROM wcf" . WCF_N . "_package_installation_sql_log " . $conditionBuilder; $statement = WCF::getDB()->prepareStatement($sql); $statement->execute($conditionBuilder->getParameters()); @@ -161,247 +197,238 @@ class DatabaseTableChangeProcessor { } /** - * Creates the given table. + * Adds the given index to the table. * - * @param DatabaseTable $table - * @throws SplitNodeException - */ - protected function createTable(DatabaseTable $table) { - $columnData = array_map(function(IDatabaseTableColumn $column) { - return [ - 'data' => $column->getData(), - 'name' => $column->getName() - ]; - }, $table->getColumns()); - $indexData = array_map(function(DatabaseTableIndex $index) { - return [ - 'data' => $index->getData(), - 'name' => $index->getName() - ]; - }, $table->getIndices()); - - $this->dbEditor->createTable($table->getName(), $columnData, $indexData); - - foreach ($table->getForeignKeys() as $foreignKey) { - $this->dbEditor->addForeignKey($table->getName(), $foreignKey->getName(), $foreignKey->getData()); - } - - $this->addedTables[] = $table; - - if ($this->oneChangePerRequest) { - $this->logChanges(); - - throw new SplitNodeException("Created table '{$table->getName()}'."); - } - } - - /** - * Drops the given table. - * - * @param DatabaseTable $table - * @throws SplitNodeException + * @param string $tableName + * @param DatabaseTableForeignKey $foreignKey */ - protected function dropTable(DatabaseTable $table) { - $this->dbEditor->dropTable($table->getName()); - - $this->droppedTables[] = $table; - - if ($this->oneChangePerRequest) { - $this->logChanges(); - - throw new SplitNodeException("Dropped table '{$table->getName()}'."); - } + protected function addForeignKey($tableName, DatabaseTableForeignKey $foreignKey) { + $this->dbEditor->addForeignKey($tableName, $foreignKey->getName(), $foreignKey->getData()); } /** - * Returns the id of the package to with the given column belongs to. If there is no specific - * log entry for the given column, the table log is checked and the relevant package id of - * the whole table is returned. If the package of the table is also unknown, `null` is returned. - * - * @param DatabaseTable $table - * @param IDatabaseTableColumn $column - * @return null|int + * Adds the given index to the table. + * + * @param string $tableName + * @param DatabaseTableIndex $index */ - protected function getColumnPackageID(DatabaseTable $table, IDatabaseTableColumn $column) { - if (isset($this->columnPackageIDs[$table->getName()][$column->getName()])) { - return $this->columnPackageIDs[$table->getName()][$column->getName()]; - } - else if (isset($this->tablePackageIDs[$table->getName()])) { - return $this->tablePackageIDs[$table->getName()]; - } - - return null; + protected function addIndex($tableName, DatabaseTableIndex $index) { + $this->dbEditor->addIndex($tableName, $index->getName(), $index->getData()); } /** - * Returns the id of the package to with the given foreign key belongs to. If there is no specific - * log entry for the given foreign key, the table log is checked and the relevant package id of - * the whole table is returned. If the package of the table is also unknown, `null` is returned. + * Applies all of the previously determined changes to achieve the desired database layout. * - * @param DatabaseTable $table - * @param DatabaseTableForeignKey $foreignKey - * @return null|int + * @throws SplitNodeException if any change has been applied */ - protected function getForeignKeyPackageID(DatabaseTable $table, DatabaseTableForeignKey $foreignKey) { - if (isset($this->foreignKeyPackageIDs[$table->getName()][$foreignKey->getName()])) { - return $this->foreignKeyPackageIDs[$table->getName()][$foreignKey->getName()]; - } - else if (isset($this->tablePackageIDs[$table->getName()])) { - return $this->tablePackageIDs[$table->getName()]; - } + protected function applyChanges() { + $appliedAnyChange = false; - return null; - } - - /** - * Returns the id of the package to with the given index belongs to. If there is no specific - * log entry for the given index, the table log is checked and the relevant package id of - * the whole table is returned. If the package of the table is also unknown, `null` is returned. - * - * @param DatabaseTable $table - * @param DatabaseTableIndex $index - * @return null|int - */ - protected function getIndexPackageID(DatabaseTable $table, DatabaseTableIndex $index) { - if (isset($this->indexPackageIDs[$table->getName()][$index->getName()])) { - return $this->indexPackageIDs[$table->getName()][$index->getName()]; - } - else if (isset($this->tablePackageIDs[$table->getName()])) { - return $this->tablePackageIDs[$table->getName()]; + foreach ($this->tablesToCreate as $table) { + $appliedAnyChange = true; + + $this->prepareTableLog($table); + $this->createTable($table); + $this->finalizeTableLog($table); } - return null; - } - - /** - * Logs all of the executed changes. - */ - protected function logChanges() { - if (!empty($this->droppedTables)) { - $sql = "DELETE FROM wcf".WCF_N."_package_installation_sql_log - WHERE sqlTable = ?"; - $statement = WCF::getDB()->prepareStatement($sql); + foreach ($this->tablesToDrop as $table) { + $appliedAnyChange = true; - WCF::getDB()->beginTransaction(); - foreach ($this->droppedTables as $table) { - $statement->execute([$table->getName()]); - } - WCF::getDB()->commitTransaction(); + $this->dropTable($table); + $this->deleteTableLog($table); } - if (!empty($this->droppedColumns)) { - $sql = "DELETE FROM wcf".WCF_N."_package_installation_sql_log - WHERE sqlTable = ? - AND sqlColumn = ?"; - $statement = WCF::getDB()->prepareStatement($sql); + $columnTables = array_unique(array_merge( + array_keys($this->columnsToAdd), + array_keys($this->columnsToAlter), + array_keys($this->columnsToDrop) + )); + foreach ($columnTables as $tableName) { + $appliedAnyChange = true; - WCF::getDB()->beginTransaction(); - foreach ($this->droppedColumns as $tableName => $columns) { - foreach ($columns as $column) { - $statement->execute([$tableName, $column->getName()]); - } + $columnsToAdd = $this->columnsToAdd[$tableName] ?? []; + $columnsToAlter = $this->columnsToAlter[$tableName] ?? []; + $columnsToDrop = $this->columnsToDrop[$tableName] ?? []; + + foreach ($columnsToAdd as $column) { + $this->prepareColumnLog($tableName, $column); } - WCF::getDB()->commitTransaction(); - } - - if (!empty($this->droppedIndices)) { - $sql = "DELETE FROM wcf".WCF_N."_package_installation_sql_log - WHERE sqlTable = ? - AND sqlIndex = ?"; - $statement = WCF::getDB()->prepareStatement($sql); - WCF::getDB()->beginTransaction(); - foreach ($this->droppedIndices as $tableName => $indices) { - foreach ($indices as $index) { - $statement->execute([$tableName, $index->getName()]); - } + $this->applyColumnChanges( + $tableName, + $columnsToAdd, + $columnsToAlter, + $columnsToDrop + ); + + foreach ($columnsToAdd as $column) { + $this->finalizeColumnLog($tableName, $column); + } + + foreach ($columnsToDrop as $column) { + $this->deleteColumnLog($tableName, $column); } - WCF::getDB()->commitTransaction(); } - $insertionData = []; - foreach ($this->addedTables as $table) { - $insertionData[] = ['sqlTable' => $table->getName()]; + foreach ($this->foreignKeysToAdd as $tableName => $foreignKeys) { + foreach ($foreignKeys as $foreignKey) { + $appliedAnyChange = true; + + $this->prepareForeignKeyLog($tableName, $foreignKey); + $this->addForeignKey($tableName, $foreignKey); + $this->finalizeForeignKeyLog($tableName, $foreignKey); + } } - foreach ($this->addedColumns as $tableName => $columns) { - foreach ($columns as $column) { - $insertionData[] = ['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]; + foreach ($this->foreignKeysToDrop as $tableName => $foreignKeys) { + foreach ($foreignKeys as $foreignKey) { + $appliedAnyChange = true; + + $this->dropForeignKey($tableName, $foreignKey); + $this->deleteForeignKeyLog($tableName, $foreignKey); } } - foreach ($this->addedIndices as $tableName => $indices) { + foreach ($this->indicesToAdd as $tableName => $indices) { foreach ($indices as $index) { - $insertionData[] = ['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]; + $appliedAnyChange = true; + + $this->prepareIndexLog($tableName, $index); + $this->addIndex($tableName, $index); + $this->finalizeIndexLog($tableName, $index); } } - if (!empty($insertionData)) { - $sql = "INSERT INTO wcf".WCF_N."_package_installation_sql_log - (packageID, sqlTable, sqlColumn, sqlIndex) - VALUES (?, ?, ?, ?)"; - $statement = WCF::getDB()->prepareStatement($sql); - - WCF::getDB()->beginTransaction(); - foreach ($insertionData as $data) { - $statement->execute([ - $this->package->packageID, - $data['sqlTable'], - $data['sqlColumn'] ?? '', - $data['sqlIndex'] ?? '' - ]); + foreach ($this->indicesToDrop as $tableName => $indices) { + foreach ($indices as $index) { + $appliedAnyChange = true; + + $this->dropIndex($tableName, $index); + $this->deleteIndexLog($tableName, $index); } - WCF::getDB()->commitTransaction(); + } + + if ($appliedAnyChange) { + throw new SplitNodeException($this->splitNodeMessage); } } /** - * Processes all tables and updates the current table layouts to match the specified layouts. + * Adds, alters, and drop columns of the same table. * - * @throws \RuntimeException if validation of the required layout changes fails + * Before a column is dropped, all of its foreign keys are dropped. + * + * @param string $tableName + * @param IDatabaseTableColumn[] $addedColumns + * @param IDatabaseTableColumn[] $alteredColumns + * @param IDatabaseTableColumn[] $droppedColumns */ - public function process() { - $errors = $this->validate(); - if (!empty($errors)) { - throw new \RuntimeException(WCF::getLanguage()->getDynamicVariable('wcf.acp.package.error.databaseChange', [ - 'errors' => $errors - ])); + protected function applyColumnChanges($tableName, array $addedColumns, array $alteredColumns, array $droppedColumns) { + $dropForeignKeys = []; + + $columnData = []; + foreach ($droppedColumns as $droppedColumn) { + $columnData[$droppedColumn->getName()] = [ + 'action' => 'drop' + ]; + + foreach ($this->getExistingTable($tableName)->getForeignKeys() as $foreignKey) { + if (in_array($droppedColumn->getName(), $foreignKey->getColumns())) { + $dropForeignKeys[] = $foreignKey->getName(); + } + } + } + foreach ($addedColumns as $addedColumn) { + $columnData[$addedColumn->getName()] = [ + 'action' => 'add', + 'data' => $addedColumn->getData() + ]; + } + foreach ($alteredColumns as $alteredColumn) { + $columnData[$alteredColumn->getName()] = [ + 'action' => 'alter', + 'data' => $alteredColumn->getData(), + 'oldColumnName' => $alteredColumn->getName() + ]; } + if (!empty($columnData)) { + foreach ($dropForeignKeys as $foreignKey) { + $this->dbEditor->dropForeignKey($tableName, $foreignKey); + } + + $this->dbEditor->alterColumns($tableName, $columnData); + } + } + + /** + * Calculates all of the necessary changes to be executed. + */ + protected function calculateChanges() { foreach ($this->tables as $table) { + $tableName = $table->getName(); + if ($table->willBeDropped()) { - if (in_array($table->getName(), $this->existingTableNames)) { - $this->dropTable($table); + if (in_array($tableName, $this->existingTableNames)) { + $this->tablesToDrop[] = $table; + + if ($this->oneChangePerRequest) { + $this->splitNodeMessage .= "Dropped table '{$tableName}'."; + break; + } + } + else if (isset($this->tablePackageIDs[$tableName])) { + $this->deleteTableLog($table); } } - else if (!in_array($table->getName(), $this->existingTableNames)) { - $this->createTable($table); + else if (!in_array($tableName, $this->existingTableNames)) { + $this->tablesToCreate[] = $table; + + if ($this->oneChangePerRequest) { + $this->splitNodeMessage .= "Created table '{$tableName}'."; + break; + } } else { // calculate difference between tables - $existingTable = DatabaseTable::createFromExistingTable($this->dbEditor, $table->getName()); + $existingTable = $this->getExistingTable($tableName); $existingColumns = $existingTable->getColumns(); - $existingForeignKeys = $existingTable->getForeignKeys(); - $existingIndices = $existingTable->getIndices(); - $addedColumns = $alteredColumns = $droppedColumns = []; foreach ($table->getColumns() as $column) { - if (!isset($existingColumns[$column->getName()]) && !$column->willBeDropped()) { - $addedColumns[$column->getName()] = $column; + if ($column->willBeDropped()) { + if (isset($existingColumns[$column->getName()])) { + if (!isset($this->columnsToDrop[$tableName])) { + $this->columnsToDrop[$tableName] = []; + } + $this->columnsToDrop[$tableName][] = $column; + } + else if (isset($this->columnPackageIDs[$tableName][$column->getName()])) { + $this->deleteColumnLog($tableName, $column); + } } - else if (isset($existingColumns[$column->getName()])) { - if ($column->willBeDropped()) { - $droppedColumns[$column->getName()] = $column; + else if (!isset($existingColumns[$column->getName()])) { + if (!isset($this->columnsToAdd[$tableName])) { + $this->columnsToAdd[$tableName] = []; } - else if (!empty(array_diff($column->getData(), $existingColumns[$column->getName()]->getData()))) { - $alteredColumns[$column->getName()] = $column; + $this->columnsToAdd[$tableName][] = $column; + } + else if (!empty(array_diff($column->getData(), $existingColumns[$column->getName()]->getData()))) { + if (!isset($this->columnsToAlter[$tableName])) { + $this->columnsToAlter[$tableName] = []; } + $this->columnsToAlter[$tableName][] = $column; } } - $this->processColumns($table, $addedColumns, $alteredColumns, $droppedColumns); + // all column-related changes are executed in one query thus break + // here and not within the previous loop + if ($this->oneChangePerRequest && (!empty($this->columnsToAdd) || !empty($this->columnsToAlter) || !empty($this->columnsToDrop))) { + $this->splitNodeMessage .= "Altered columns of table '{$tableName}'."; + break; + } - $addedForeignKeys = $droppedForeignKeys = []; + $existingForeignKeys = $existingTable->getForeignKeys(); foreach ($table->getForeignKeys() as $foreignKey) { $matchingExistingForeignKey = null; foreach ($existingForeignKeys as $existingForeignKey) { @@ -413,17 +440,34 @@ class DatabaseTableChangeProcessor { if ($foreignKey->willBeDropped()) { if ($matchingExistingForeignKey !== null) { - $droppedForeignKeys[$foreignKey->getName()] = $foreignKey; + if (!isset($this->foreignKeysToDrop[$tableName])) { + $this->foreignKeysToDrop[$tableName] = []; + } + $this->foreignKeysToDrop[$tableName][] = $foreignKey; + + if ($this->oneChangePerRequest) { + $this->splitNodeMessage .= "Dropped foreign key '{$tableName}." . implode(',', $foreignKey->getColumns()) . "'."; + break 2; + } + } + else if (isset($this->foreignKeyPackageIDs[$tableName][$foreignKey->getName()])) { + $this->deleteForeignKeyLog($tableName, $foreignKey); } } else if ($matchingExistingForeignKey === null) { - $addedForeignKeys[$foreignKey->getName()] = $foreignKey; + if (!isset($this->foreignKeysToAdd[$tableName])) { + $this->foreignKeysToAdd[$tableName] = []; + } + $this->foreignKeysToAdd[$tableName][] = $foreignKey; + + if ($this->oneChangePerRequest) { + $this->splitNodeMessage .= "Added foreign key '{$tableName}." . implode(',', $foreignKey->getColumns()) . "'."; + break 2; + } } } - $this->processForeignKeys($table, $addedForeignKeys, $droppedForeignKeys); - - $addedIndices = $droppedIndices = []; + $existingIndices = $existingTable->getIndices(); foreach ($table->getIndices() as $index) { $matchingExistingIndex = null; foreach ($existingIndices as $existingIndex) { @@ -435,151 +479,445 @@ class DatabaseTableChangeProcessor { if ($index->willBeDropped()) { if ($matchingExistingIndex !== null) { - $droppedIndices[$index->getName()] = $index; + if (!isset($this->indicesToDrop[$tableName])) { + $this->indicesToDrop[$tableName] = []; + } + $this->indicesToDrop[$tableName][] = $index; + + if ($this->oneChangePerRequest) { + $this->splitNodeMessage .= "Dropped index '{$tableName}." . implode(',', $index->getColumns()) . "'."; + break 2; + } + } + else if (isset($this->indexPackageIDs[$tableName][$index->getName()])) { + $this->deleteIndexLog($tableName, $index); } } else if ($matchingExistingIndex === null) { - $addedIndices[$index->getName()] = $index; + if (!isset($this->indicesToAdd[$tableName])) { + $this->indicesToAdd[$tableName] = []; + } + $this->indicesToAdd[$tableName][] = $index; + + if ($this->oneChangePerRequest) { + $this->splitNodeMessage .= "Added index '{$tableName}." . implode(',', $index->getColumns()) . "'."; + break 2; + } } } - - $this->processIndices($table, $addedIndices, $droppedIndices); } } + } + + /** + * Checks for any pending log entries for the package and either marks them as done or + * deletes them so that after this method finishes, there are no more undone log entries + * for the package. + */ + protected function checkPendingLogEntries() { + $sql = "SELECT * + FROM wcf" . WCF_N . "_package_installation_sql_log + WHERE packageID = ? + AND isDone = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([$this->package->packageID, 0]); - $this->logChanges(); + $doneEntries = $undoneEntries = []; + while ($row = $statement->fetchArray()) { + // table + if ($row['sqlIndex'] === '' && $row['sqlColumn'] === '') { + if (in_array($row['sqlTable'], $this->existingTableNames)) { + $doneEntries[] = $row; + } + else { + $undoneEntries[] = $row; + } + } + // column + else if ($row['sqlIndex'] === '') { + if (isset($this->getExistingTable($row['sqlTable'])->getColumns()[$row['sqlColumn']])) { + $doneEntries[] = $row; + } + else { + $undoneEntries[] = $row; + } + } + // foreign key + else if (substr($row['sqlIndex'], -3) === '_fk') { + if (isset($this->getExistingTable($row['sqlTable'])->getForeignKeys()[$row['sqlIndex']])) { + $doneEntries[] = $row; + } + else { + $undoneEntries[] = $row; + } + } + // index + else { + if (isset($this->getExistingTable($row['sqlTable'])->getIndices()[$row['sqlIndex']])) { + $doneEntries[] = $row; + } + else { + $undoneEntries[] = $row; + } + } + } + + WCF::getDB()->beginTransaction(); + foreach ($doneEntries as $entry) { + $this->finalizeLog($entry); + } + + // to achieve a consistent state, undone log entries will be deleted here even though + // they might be re-created later to ensure that after this method finishes, there are + // no more undone entries in the log for the relevant package + foreach ($undoneEntries as $entry) { + $this->deleteLog($entry); + } + WCF::getDB()->commitTransaction(); } /** - * Adds, alters and drops the given columns. + * Creates the given table. * - * @param DatabaseTable $table - * @param IDatabaseTableColumn[] $addedColumns - * @param IDatabaseTableColumn[] $alteredColumns - * @param IDatabaseTableColumn[] $droppedColumns - * @throws SplitNodeException + * @param DatabaseTable $table */ - protected function processColumns(DatabaseTable $table, array $addedColumns, array $alteredColumns, array $droppedColumns) { - $columnData = []; - foreach ($droppedColumns as $droppedColumn) { - $columnData[$droppedColumn->getName()] = [ - 'action' => 'drop' + protected function createTable(DatabaseTable $table) { + $columnData = array_map(function(IDatabaseTableColumn $column) { + return [ + 'data' => $column->getData(), + 'name' => $column->getName() ]; - - $this->droppedColumns[$table->getName()][$droppedColumn->getName()] = $droppedColumn; - } - foreach ($addedColumns as $addedColumn) { - $columnData[$addedColumn->getName()] = [ - 'action' => 'add', - 'data' => $addedColumn->getData() + }, $table->getColumns()); + $indexData = array_map(function(DatabaseTableIndex $index) { + return [ + 'data' => $index->getData(), + 'name' => $index->getName() ]; - - if ($this->tablePackageIDs[$table->getName()] !== $this->package->packageID) { - $this->addedColumns[$table->getName()][$addedColumn->getName()] = $addedColumn; - } + }, $table->getIndices()); + + $this->dbEditor->createTable($table->getName(), $columnData, $indexData); + + foreach ($table->getForeignKeys() as $foreignKey) { + $this->dbEditor->addForeignKey($table->getName(), $foreignKey->getName(), $foreignKey->getData()); } - foreach ($alteredColumns as $alteredColumn) { - $columnData[$alteredColumn->getName()] = [ - 'action' => 'alter', - 'data' => $alteredColumn->getData(), - 'oldColumnName' => $alteredColumn->getName() - ]; + } + + /** + * Deletes the log entry for the given column. + * + * @param string $tableName + * @param IDatabaseTableColumn $column + */ + protected function deleteColumnLog($tableName, IDatabaseTableColumn $column) { + $this->deleteLog(['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]); + } + + /** + * Deletes the log entry for the given foreign key. + * + * @param string $tableName + * @param DatabaseTableForeignKey $foreignKey + */ + protected function deleteForeignKeyLog($tableName, DatabaseTableForeignKey $foreignKey) { + $this->deleteLog(['sqlTable' => $tableName, 'sqlIndex' => $foreignKey->getName()]); + } + + /** + * Deletes the log entry for the given index. + * + * @param string $tableName + * @param DatabaseTableIndex $index + */ + protected function deleteIndexLog($tableName, DatabaseTableIndex $index) { + $this->deleteLog(['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]); + } + + /** + * Deletes a log entry. + * + * @param array $data + */ + protected function deleteLog(array $data) { + $sql = "DELETE FROM wcf" . WCF_N . "_package_installation_sql_log + WHERE packageID = ? + AND sqlTable = ? + AND sqlColumn = ? + AND sqlIndex = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + + $statement->execute([ + $this->package->packageID, + $data['sqlTable'], + $data['sqlColumn'] ?? '', + $data['sqlIndex'] ?? '' + ]); + } + + /** + * Deletes all log entry related to the given table. + * + * @param DatabaseTable $table + */ + protected function deleteTableLog(DatabaseTable $table) { + $sql = "DELETE FROM wcf" . WCF_N . "_package_installation_sql_log + WHERE packageID = ? + AND sqlTable = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + + $statement->execute([ + $this->package->packageID, + $table->getName() + ]); + } + + /** + * Drops the given foreign key. + * + * @param string $tableName + * @param DatabaseTableForeignKey $foreignKey + */ + protected function dropForeignKey($tableName, DatabaseTableForeignKey $foreignKey) { + $this->dbEditor->dropForeignKey($tableName, $foreignKey->getName()); + $this->dbEditor->dropIndex($tableName, $foreignKey->getName()); + } + + /** + * Drops the given index. + * + * @param string $tableName + * @param DatabaseTableIndex $index + */ + protected function dropIndex($tableName, DatabaseTableIndex $index) { + $this->dbEditor->dropIndex($tableName, $index->getName()); + } + + /** + * Drops the given table. + * + * @param DatabaseTable $table + */ + protected function dropTable(DatabaseTable $table) { + $this->dbEditor->dropTable($table->getName()); + } + + /** + * Finalizes the log entry for the creation of the given column. + * + * @param string $tableName + * @param IDatabaseTableColumn $column + */ + protected function finalizeColumnLog($tableName, IDatabaseTableColumn $column) { + $this->finalizeLog(['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]); + } + + /** + * Finalizes the log entry for adding the given index. + * + * @param string $tableName + * @param DatabaseTableForeignKey $foreignKey + */ + protected function finalizeForeignKeyLog($tableName, DatabaseTableForeignKey $foreignKey) { + $this->finalizeLog(['sqlTable' => $tableName, 'sqlIndex' => $foreignKey->getName()]); + } + + /** + * Finalizes the log entry for adding the given index. + * + * @param string $tableName + * @param DatabaseTableIndex $index + */ + protected function finalizeIndexLog($tableName, DatabaseTableIndex $index) { + $this->finalizeLog(['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]); + } + + /** + * Finalizes a log entry after the relevant change has been executed. + * + * @param array $data + */ + protected function finalizeLog(array $data) { + $sql = "UPDATE wcf" . WCF_N . "_package_installation_sql_log + SET isDone = ? + WHERE packageID = ? + AND sqlTable = ? + AND sqlColumn = ? + AND sqlIndex = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + + $statement->execute([ + 1, + $this->package->packageID, + $data['sqlTable'], + $data['sqlColumn'] ?? '', + $data['sqlIndex'] ?? '' + ]); + } + + /** + * Finalizes the log entry for the creation of the given table. + * + * @param DatabaseTable $table + */ + protected function finalizeTableLog(DatabaseTable $table) { + $this->finalizeLog(['sqlTable' => $table->getName()]); + } + + /** + * Returns the id of the package to with the given column belongs to. If there is no specific + * log entry for the given column, the table log is checked and the relevant package id of + * the whole table is returned. If the package of the table is also unknown, `null` is returned. + * + * @param DatabaseTable $table + * @param IDatabaseTableColumn $column + * @return null|int + */ + protected function getColumnPackageID(DatabaseTable $table, IDatabaseTableColumn $column) { + if (isset($this->columnPackageIDs[$table->getName()][$column->getName()])) { + return $this->columnPackageIDs[$table->getName()][$column->getName()]; + } + else if (isset($this->tablePackageIDs[$table->getName()])) { + return $this->tablePackageIDs[$table->getName()]; } - if (!empty($columnData)) { - $this->dbEditor->alterColumns($table->getName(), $columnData); - - if ($this->oneChangePerRequest) { - $this->logChanges(); - - throw new SplitNodeException("Altered columns of table '{$table->getName()}'."); - } + return null; + } + + /** + * Returns the `DatabaseTable` object for the table with the given name. + * + * @param string $tableName + * @return DatabaseTable + */ + protected function getExistingTable($tableName) { + if (!isset($this->existingTables[$tableName])) { + $this->existingTables[$tableName] = DatabaseTable::createFromExistingTable($this->dbEditor, $tableName); } + + return $this->existingTables[$tableName]; } /** - * Adds and drops the given foreign keys. + * Returns the id of the package to with the given foreign key belongs to. If there is no specific + * log entry for the given foreign key, the table log is checked and the relevant package id of + * the whole table is returned. If the package of the table is also unknown, `null` is returned. * * @param DatabaseTable $table - * @param DatabaseTableForeignKey[] $addedForeignKeys - * @param DatabaseTableForeignKey[] $droppedForeignKeys - * @throws SplitNodeException + * @param DatabaseTableForeignKey $foreignKey + * @return null|int */ - protected function processForeignKeys(DatabaseTable $table, array $addedForeignKeys, array $droppedForeignKeys) { - if (empty($addedForeignKeys) && empty($droppedForeignKeys)) { - return; + protected function getForeignKeyPackageID(DatabaseTable $table, DatabaseTableForeignKey $foreignKey) { + if (isset($this->foreignKeyPackageIDs[$table->getName()][$foreignKey->getName()])) { + return $this->foreignKeyPackageIDs[$table->getName()][$foreignKey->getName()]; } - - foreach ($addedForeignKeys as $addedForeignKey) { - if ($this->tablePackageIDs[$table->getName()] !== $this->package->packageID) { - $this->addedIndices[$table->getName()][$addedForeignKey->getName()] = $addedForeignKey; - } - - $this->dbEditor->addForeignKey($table->getName(), $addedForeignKey->getName(), $addedForeignKey->getData()); - - if ($this->oneChangePerRequest) { - $this->logChanges(); - - throw new SplitNodeException("Added foreign key '{$table->getName()}." . implode(',', $addedForeignKey->getColumns()) . "'"); - } + else if (isset($this->tablePackageIDs[$table->getName()])) { + return $this->tablePackageIDs[$table->getName()]; } - foreach ($droppedForeignKeys as $droppedForeignKey) { - $this->droppedIndices[$table->getName()][$droppedForeignKey->getName()] = $droppedForeignKey; - - $this->dbEditor->dropForeignKey($table->getName(), $droppedForeignKey->getName()); - - if ($this->oneChangePerRequest) { - $this->logChanges(); - - throw new SplitNodeException("Dropped foreign key '{$table->getName()}." . implode(',', $droppedForeignKey->getColumns()) . "' ({$droppedForeignKey->getName()})"); - } - } + return null; } /** - * Adds and drops the given indices. + * Returns the id of the package to with the given index belongs to. If there is no specific + * log entry for the given index, the table log is checked and the relevant package id of + * the whole table is returned. If the package of the table is also unknown, `null` is returned. * * @param DatabaseTable $table - * @param DatabaseTableIndex[] $addedIndices - * @param DatabaseTableIndex[] $droppedIndices - * @throws SplitNodeException + * @param DatabaseTableIndex $index + * @return null|int */ - protected function processIndices(DatabaseTable $table, array $addedIndices, array $droppedIndices) { - if (empty($addedIndices) && empty($droppedIndices)) { - return; + protected function getIndexPackageID(DatabaseTable $table, DatabaseTableIndex $index) { + if (isset($this->indexPackageIDs[$table->getName()][$index->getName()])) { + return $this->indexPackageIDs[$table->getName()][$index->getName()]; } - - foreach ($addedIndices as $addedIndex) { - if ($this->tablePackageIDs[$table->getName()] !== $this->package->packageID) { - $this->addedIndices[$table->getName()][$addedIndex->getName()] = $addedIndex; - } - - $this->dbEditor->addIndex($table->getName(), $addedIndex->getName(), $addedIndex->getData()); - - if ($this->oneChangePerRequest) { - $this->logChanges(); - - throw new SplitNodeException("Added index '{$table->getName()}." . implode(',', $addedIndex->getColumns()) . "'"); - } + else if (isset($this->tablePackageIDs[$table->getName()])) { + return $this->tablePackageIDs[$table->getName()]; } - foreach ($droppedIndices as $droppedIndex) { - $this->droppedIndices[$table->getName()][$droppedIndex->getName()] = $droppedIndex; - - $this->dbEditor->dropIndex($table->getName(), $droppedIndex->getName()); - - if ($this->oneChangePerRequest) { - $this->logChanges(); - - throw new SplitNodeException("Dropped index '{$table->getName()}." . implode(',', $droppedIndex->getColumns()) . "'"); - } + return null; + } + + /** + * Prepares the log entry for the creation of the given column. + * + * @param string $tableName + * @param IDatabaseTableColumn $column + */ + protected function prepareColumnLog($tableName, IDatabaseTableColumn $column) { + $this->prepareLog(['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]); + } + + /** + * Prepares the log entry for adding the given foreign key. + * + * @param string $tableName + * @param DatabaseTableForeignKey $foreignKey + */ + protected function prepareForeignKeyLog($tableName, DatabaseTableForeignKey $foreignKey) { + $this->prepareLog(['sqlTable' => $tableName, 'sqlIndex' => $foreignKey->getName()]); + } + + /** + * Prepares the log entry for adding the given index. + * + * @param string $tableName + * @param DatabaseTableIndex $index + */ + protected function prepareIndexLog($tableName, DatabaseTableIndex $index) { + $this->prepareLog(['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]); + } + + /** + * Prepares a log entry before the relevant change has been executed. + * + * @param array $data + */ + protected function prepareLog(array $data) { + $sql = "INSERT INTO wcf" . WCF_N . "_package_installation_sql_log + (packageID, sqlTable, sqlColumn, sqlIndex, isDone) + VALUES (?, ?, ?, ?, ?)"; + $statement = WCF::getDB()->prepareStatement($sql); + + $statement->execute([ + $this->package->packageID, + $data['sqlTable'], + $data['sqlColumn'] ?? '', + $data['sqlIndex'] ?? '', + 0 + ]); + } + + /** + * Prepares the log entry for the creation of the given table. + * + * @param DatabaseTable $table + */ + protected function prepareTableLog(DatabaseTable $table) { + $this->prepareLog(['sqlTable' => $table->getName()]); + } + + /** + * Processes all tables and updates the current table layouts to match the specified layouts. + * + * @throws \RuntimeException if validation of the required layout changes fails + */ + public function process() { + $this->checkPendingLogEntries(); + + $errors = $this->validate(); + if (!empty($errors)) { + throw new \RuntimeException(WCF::getLanguage()->getDynamicVariable('wcf.acp.package.error.databaseChange', [ + 'errors' => $errors + ])); } + + $this->calculateChanges(); + + $this->applyChanges(); } /** * Checks if the relevant table layout changes can be executed and returns an array with information - * on any validation error. + * on all validation errors. * * @return array */ diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 5a1893f4b6..abbee2fea1 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -9,6 +9,7 @@ CREATE TABLE wcf1_package_installation_sql_log ( sqlTable VARCHAR(100) NOT NULL DEFAULT '', sqlColumn VARCHAR(100) NOT NULL DEFAULT '', sqlIndex VARCHAR(100) NOT NULL DEFAULT '', + isDone TINYINT(1) NOT NULL DEFAULT 1, UNIQUE KEY packageID (packageID, sqlTable, sqlColumn, sqlIndex) );