*/
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
*/
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
*/
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[]
*/
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`.
*
$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());
}
/**
- * 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) {
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) {
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
*/