Update logging procedure of database change
authorMatthias Schmidt <gravatronics@live.com>
Sun, 8 Sep 2019 13:11:11 +0000 (15:11 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Sun, 8 Sep 2019 13:11:11 +0000 (15:11 +0200)
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.)

wcfsetup/install/files/lib/system/database/table/DatabaseTableChangeProcessor.class.php
wcfsetup/setup/db/install.sql

index 7fe862cb161be3a0eeb2beb1cf1dc3de5fe10576..6b8d37c3b5577648c3d0058e699d83b65fa45562 100644 (file)
@@ -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
         */
index 5a1893f4b6a484dd525eeba0297c8013de98d6d9..abbee2fea19fde43b7baca6c1a4835b0dd3f4a91 100644 (file)
@@ -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) 
 );