Add PHP API to update database tables
authorMatthias Schmidt <gravatronics@live.com>
Sun, 1 Sep 2019 16:26:20 +0000 (18:26 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Sun, 1 Sep 2019 16:26:36 +0000 (18:26 +0200)
See #2847

57 files changed:
wcfsetup/install/files/acp/update-com.woltlab.wcf_5.2.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/editor/DatabaseEditor.class.php
wcfsetup/install/files/lib/system/database/editor/MySQLDatabaseEditor.class.php
wcfsetup/install/files/lib/system/database/table/DatabaseTable.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/DatabaseTableChangeProcessor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/TDroppableDatabaseComponent.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/AbstractDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/AbstractDecimalDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/AbstractIntDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/BigintDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/BinaryDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/BlobDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/CharDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/DateDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/DatetimeDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/DecimalDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/DefaultFalseBooleanDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/DefaultTrueBooleanDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/DoubleDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/EnumDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/FloatDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/IAutoIncrementDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/IDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/IDecimalsDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/IEnumDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/ILengthDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/IUnsignedDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/IntDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/LongblobDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/LongtextDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/MediumblobDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/MediumintDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/MediumtextDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/NotNullInt10DatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/NotNullVarchar191DatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/NotNullVarchar255DatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/ObjectIdDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/SetDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/SmallintDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/TAutoIncrementDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/TDecimalsDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/TEnumDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/TLengthDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/TUnsignedDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/TextDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/TimeDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/TinyblobDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/TinyintDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/TinytextDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/VarbinaryDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/VarcharDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/column/YearDatabaseTableColumn.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/index/DatabaseTableForeignKey.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/index/DatabaseTableIndex.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/database/table/index/DatabaseTablePrimaryIndex.class.php [new file with mode: 0644]
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

diff --git a/wcfsetup/install/files/acp/update-com.woltlab.wcf_5.2.php b/wcfsetup/install/files/acp/update-com.woltlab.wcf_5.2.php
new file mode 100644 (file)
index 0000000..6884f7a
--- /dev/null
@@ -0,0 +1,203 @@
+<?php
+use wcf\system\database\table\column\BinaryDatabaseTableColumn;
+use wcf\system\database\table\column\CharDatabaseTableColumn;
+use wcf\system\database\table\column\DateDatabaseTableColumn;
+use wcf\system\database\table\column\DatetimeDatabaseTableColumn;
+use wcf\system\database\table\column\DefaultFalseBooleanDatabaseTableColumn;
+use wcf\system\database\table\column\EnumDatabaseTableColumn;
+use wcf\system\database\table\column\IntDatabaseTableColumn;
+use wcf\system\database\table\column\MediumtextDatabaseTableColumn;
+use wcf\system\database\table\column\NotNullInt10DatabaseTableColumn;
+use wcf\system\database\table\column\NotNullVarchar255DatabaseTableColumn;
+use wcf\system\database\table\column\ObjectIdDatabaseTableColumn;
+use wcf\system\database\table\column\SmallintDatabaseTableColumn;
+use wcf\system\database\table\column\TextDatabaseTableColumn;
+use wcf\system\database\table\DatabaseTable;
+use wcf\system\database\table\DatabaseTableChangeProcessor;
+use wcf\system\database\table\index\DatabaseTableForeignKey;
+use wcf\system\database\table\index\DatabaseTableIndex;
+use wcf\system\database\table\index\DatabaseTablePrimaryIndex;
+use wcf\system\package\plugin\ScriptPackageInstallationPlugin;
+use wcf\system\WCF;
+
+/**
+ * Updates the database table layout from WoltLab Suite Core 3.1 to 5.2.
+ * 
+ * TODO: untested, potentially incomplete
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ */
+
+$tables = [
+       DatabaseTable::create('wcf1_bbcode_media_provider')
+               ->columns([
+                       DefaultFalseBooleanDatabaseTableColumn::create('isDisabled')
+               ]),
+       
+       DatabaseTable::create('wcf1_blacklist_status')
+               ->columns([
+                       DateDatabaseTableColumn::create('date')
+                               ->notNull(),
+                       DefaultFalseBooleanDatabaseTableColumn::create('delta1'),
+                       DefaultFalseBooleanDatabaseTableColumn::create('delta2'),
+                       DefaultFalseBooleanDatabaseTableColumn::create('delta3'),
+                       DefaultFalseBooleanDatabaseTableColumn::create('delta4')
+               ]),
+       
+       DatabaseTable::create('wcf1_blacklist_entry')
+               ->columns([
+                       EnumDatabaseTableColumn::create('type')
+                               ->enumValues(['email', 'ipv4', 'ipv6','username']),
+                       BinaryDatabaseTableColumn::create('hash')
+                               ->length(32),
+                       DatetimeDatabaseTableColumn::create('lastSeen')
+                               ->notNull(),
+                       SmallintDatabaseTableColumn::create('occurrences')
+                               ->length(5)
+                               ->notNull()
+               ])
+               ->indices([
+                       DatabaseTableIndex::create('entry')
+                               ->type(DatabaseTableIndex::UNIQUE_TYPE)
+                               ->columns(['type', 'hash']),
+                       DatabaseTableIndex::create('numberOfReports')
+                               ->columns(['type', 'occurrences'])
+               ]),
+       
+       DatabaseTable::create('wcf1_box')
+               ->columns([
+                       DefaultFalseBooleanDatabaseTableColumn::create('isDisabled')
+               ]),
+       
+       DatabaseTable::create('wcf1_category')
+               ->columns([
+                       DefaultFalseBooleanDatabaseTableColumn::create('descriptionUseHtml')
+               ]),
+       
+       DatabaseTable::create('wcf1_comment')
+               ->columns([
+                       MediumtextDatabaseTableColumn::create('message')
+               ]),
+       
+       DatabaseTable::create('wcf1_comment_response')
+               ->columns([
+                       MediumtextDatabaseTableColumn::create('message')
+               ]),
+       
+       DatabaseTable::create('wcf1_contact_attachment')
+               ->columns([
+                       NotNullInt10DatabaseTableColumn::create('attachmentID'),
+                       CharDatabaseTableColumn::create('accessKey')
+                               ->length(40)
+                               ->notNull()
+               ])
+               ->foreignKeys([
+                       DatabaseTableForeignKey::create()
+                               ->columns(['attachmentID'])
+                               ->referencedTable('wcf1_attachment')
+                               ->referencedColumns(['attachmentID'])
+                               ->onDelete('CASCADE')
+               ]),
+       
+       DatabaseTable::create('wcf1_language_item')
+               ->columns([
+                       DefaultFalseBooleanDatabaseTableColumn::create('isCustomLanguageItem')
+               ]),
+       
+       DatabaseTable::create('wcf1_like')
+               ->columns([
+                       NotNullInt10DatabaseTableColumn::create('reactionTypeID')
+               ])
+               ->foreignKeys([
+                       DatabaseTableForeignKey::create()
+                               ->columns(['reactionTypeID'])
+                               ->referencedTable('wcf1_reaction_type')
+                               ->referencedColumns(['reactionTypeID'])
+                               ->onDelete('CASCADE')
+               ]),
+       
+       DatabaseTable::create('wcf1_like_object')
+               ->columns([
+                       TextDatabaseTableColumn::create('cachedReactions')
+               ]),
+       
+       DatabaseTable::create('wcf1_media')
+               ->columns([
+                       DefaultFalseBooleanDatabaseTableColumn::create('captionEnableHtml'),
+                       NotNullInt10DatabaseTableColumn::create('downloads')
+                               ->defaultValue(0),
+                       NotNullInt10DatabaseTableColumn::create('lastDownload')
+                               ->defaultValue(0)
+               ]),
+       
+       DatabaseTable::create('wcf1_page')
+               ->columns([
+                       IntDatabaseTableColumn::create('overrideApplicationPackageID')
+                               ->length(10),
+                       DefaultFalseBooleanDatabaseTableColumn::create('enableShareButtons')
+               ])
+               ->foreignKeys([
+                       DatabaseTableForeignKey::create()
+                               ->columns(['overrideApplicationPackageID'])
+                               ->referencedTable('wcf1_package')
+                               ->referencedColumns(['packageID'])
+                               ->onDelete('SET NULL')
+               ]),
+       
+       DatabaseTable::create('wcf1_reaction_type')
+               ->columns([
+                       ObjectIdDatabaseTableColumn::create('reactionTypeID'),
+                       // TODO: is currently not `not null`
+                       NotNullVarchar255DatabaseTableColumn::create('title'),
+                       NotNullInt10DatabaseTableColumn::create('showOrder')
+                               ->defaultValue(0),
+                       // TODO: should be varchar
+                       MediumtextDatabaseTableColumn::create('iconFile'),
+                       DefaultFalseBooleanDatabaseTableColumn::create('isDisabled')
+               ])
+               ->indices([
+                       DatabaseTablePrimaryIndex::create()
+                               ->columns(['reactionTypeID'])
+               ]),
+       
+       DatabaseTable::create('wcf1_style')
+               ->columns([
+                       EnumDatabaseTableColumn::create('apiVersion')
+                               ->enumValues(['3.0', '3.1', '5.2'])
+               ]),
+       
+       DatabaseTable::create('wcf1_trophy')
+               ->columns([
+                       DefaultFalseBooleanDatabaseTableColumn::create('revokeAutomatically'),
+                       DefaultFalseBooleanDatabaseTableColumn::create('trophyUseHtml'),
+                       NotNullInt10DatabaseTableColumn::create('showOrder')
+                               ->defaultValue(0)
+               ]),
+       
+       DatabaseTable::create('wcf1_user')
+               ->columns([
+                       NotNullInt10DatabaseTableColumn::create('articles')
+                               ->defaultValue(0),
+                       NotNullVarchar255DatabaseTableColumn::create('blacklistMatches')
+                               ->defaultValue('')
+               ]),
+       
+       DatabaseTable::create('wcf1_user_group')
+               ->columns([
+                       DefaultFalseBooleanDatabaseTableColumn::create('allowMention')
+               ]),
+       
+       DatabaseTable::create('wcf1_user_trophy')
+               ->columns([
+                       DefaultFalseBooleanDatabaseTableColumn::create('trophyUseHtml')
+               ]),
+];
+
+(new DatabaseTableChangeProcessor(
+       /** @var ScriptPackageInstallationPlugin $this */
+       $this->installation->getPackage(),
+       $tables,
+       WCF::getDB()->getEditor())
+)->process();
index 15eaf875c7960039c6d6d99c9fb7b88fd8e645de..dad7e2b17b3598b97f6cb3c3bcaf2d317eef4e3b 100644 (file)
@@ -104,6 +104,16 @@ abstract class DatabaseEditor {
         */
        abstract public function alterColumn($tableName, $oldColumnName, $newColumnName, $newColumnData);
        
+       /**
+        * Adds, alters and drops multiple columns at once.
+        * 
+        * @param       string          $tableName
+        * @param       array           $alterData
+        */
+       public function alterColumns($tableName, $alterData) {
+               throw new NotImplementedException();
+       }
+       
        /**
         * Drops an existing column.
         * 
index 3352ccceb742611c175caebaee01578001050621..406bbbeedb02af049f291e35221c1559157e936e 100644 (file)
@@ -31,7 +31,7 @@ class MySQLDatabaseEditor extends DatabaseEditor {
         */
        public function getColumns($tableName) {
                $columns = [];
-               $regex = new Regex('([a-z]+)\((.+)\)', Regex::CASE_INSENSITIVE);
+               $regex = new Regex('([a-z]+)\((.+)\)( unsigned)?', Regex::CASE_INSENSITIVE);
                
                $sql = "SHOW COLUMNS FROM `".$tableName."`";
                $statement = $this->dbObj->prepareStatement($sql);
@@ -44,6 +44,7 @@ class MySQLDatabaseEditor extends DatabaseEditor {
                        $length = '';
                        $decimals = '';
                        $enumValues = '';
+                       $unsigned = false;
                        if (!empty($typeMatches)) {
                                $type = $typeMatches[1];
                                
@@ -75,6 +76,10 @@ class MySQLDatabaseEditor extends DatabaseEditor {
                                                }
                                                break;
                                }
+                               
+                               if (isset($typeMatches[3])) {
+                                       $unsigned = true;
+                               }
                        }
                        
                        $columns[] = ['name' => $row['Field'], 'data' => [
@@ -85,7 +90,8 @@ class MySQLDatabaseEditor extends DatabaseEditor {
                                'default' => $row['Default'],
                                'autoIncrement' => $row['Extra'] == 'auto_increment' ? true : false,
                                'enumValues' => $enumValues,
-                               'decimals' => $decimals
+                               'decimals' => $decimals,
+                               'unsigned' => $unsigned
                        ]];
                }
                
@@ -258,6 +264,30 @@ class MySQLDatabaseEditor extends DatabaseEditor {
                $statement->execute();
        }
        
+       /**
+        * @inheritDoc
+        */
+       public function alterColumns($tableName, $alterData) {
+               $queries = "";
+               foreach ($alterData as $columnName => $data) {
+                       switch ($data['action']) {
+                               case 'add':
+                                       $queries .= "ADD COLUMN {$this->buildColumnDefinition($columnName, $data['data'])},";
+                                       break;
+                                       
+                               case 'alter':
+                                       $queries .= "CHANGE COLUMN `{$columnName}` {$this->buildColumnDefinition($data['oldColumnName'], $data['data'])},";
+                                       break;
+                                       
+                               case 'drop':
+                                       $queries .= "DROP COLUMN `{$columnName}`,";
+                                       break;
+                       }
+               }
+               
+               $this->dbObj->prepareStatement("ALTER TABLE `{$tableName}` " . rtrim($queries, ','))->execute();
+       }
+
        /**
         * @inheritDoc
         */
@@ -342,6 +372,11 @@ class MySQLDatabaseEditor extends DatabaseEditor {
                $definition = "`".$columnName."`";
                // column type
                $definition .= " ".$columnData['type'];
+               
+               if (!empty($columnData['unsigned'])) {
+                       $definition .= ' UNSIGNED';
+               }
+               
                // column length and decimals
                if (!empty($columnData['length'])) {
                        $definition .= "(".$columnData['length'].(!empty($columnData['decimals']) ? ",".$columnData['decimals'] : "").")";
diff --git a/wcfsetup/install/files/lib/system/database/table/DatabaseTable.class.php b/wcfsetup/install/files/lib/system/database/table/DatabaseTable.class.php
new file mode 100644 (file)
index 0000000..bc6c9c3
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+namespace wcf\system\database\table;
+use wcf\system\application\ApplicationHandler;
+use wcf\system\database\editor\DatabaseEditor;
+use wcf\system\database\table\column\IDatabaseTableColumn;
+use wcf\system\database\table\index\DatabaseTableForeignKey;
+use wcf\system\database\table\index\DatabaseTableIndex;
+
+/**
+ * PHP representation of an existing database table or the intended layout of an non-existing or
+ * existing database table.
+ * 
+ * @author     Alexander Ebert, Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table
+ * @since      5.2
+ */
+class DatabaseTable {
+       use TDroppableDatabaseComponent;
+       
+       /**
+        * intended database table's columns
+        * @var IDatabaseTableColumn[]
+        */
+       protected $columns = [];
+       
+       /**
+        * intended database table's foreign keys
+        * @var DatabaseTableForeignKey[]
+        */
+       protected $foreignKeys = [];
+       
+       /**
+        * intended database table's indices
+        * @var DatabaseTableIndex[]
+        */
+       protected $indices = [];
+       
+       /**
+        * name of the database table
+        * @var string
+        */
+       protected $name;
+       
+       /**
+        * Creates a new instance of `DatabaseTable`.
+        * 
+        * @param       string          $name   name of the database table
+        */
+       protected function __construct($name) {
+               $this->name = ApplicationHandler::insertRealDatabaseTableNames($name);
+       }
+       
+       /**
+        * Sets the columns of the database table.
+        * 
+        * @param       IDatabaseTableColumn[]  $columns        added/dropped columns
+        * @return      $this                                   this database table
+        * @throws      \InvalidArgumentException               if any column is invalid or duplicate column names exist
+        */
+       public function columns(array $columns) {
+               $this->columns = [];
+               foreach ($columns as $column) {
+                       if (!($column instanceof IDatabaseTableColumn)) {
+                               throw new \InvalidArgumentException("Added columns have to be instances of '" . IDatabaseTableColumn::class . "'.");
+                       }
+                       
+                       if (isset($this->columns[$column->getName()])) {
+                               throw new \InvalidArgumentException("Duplicate column with name '{$column->getName()}'.");
+                       }
+                       
+                       $this->columns[$column->getName()] = $column;
+               }
+               
+               return $this;
+       }
+       
+       /**
+        * Sets the foreign keys of the database table.
+        * 
+        * @param       DatabaseTableForeignKey[]       $foreignKeys    added/dropped foreign keys
+        * @return      $this                                           this database table
+        * @throws      \InvalidArgumentException                       if any foreign key is invalid or duplicate foreign key names exist
+        */
+       public function foreignKeys(array $foreignKeys) {
+               $this->foreignKeys = [];
+               foreach ($foreignKeys as $foreignKey) {
+                       if (!($foreignKey instanceof DatabaseTableForeignKey)) {
+                               throw new \InvalidArgumentException("Added foreign keys have to be instances of '" . DatabaseTableForeignKey::class . "'.");
+                       }
+                       
+                       if (empty($foreignKey->getColumns())) {
+                               throw new \InvalidArgumentException("Missing columns for foreign key.");
+                       }
+                       
+                       if ($foreignKey->getName() === '') {
+                               $foreignKey->name(md5($this->getName() . '_' . $foreignKey->getColumns()[0]) . '_fk');
+                       }
+                       
+                       if (isset($this->foreignKeys[$foreignKey->getName()])) {
+                               throw new \InvalidArgumentException("Duplicate foreign key with name '{$foreignKey->getName()}'.");
+                       }
+                       
+                       $this->foreignKeys[$foreignKey->getName()] = $foreignKey;
+               }
+               
+               return $this;
+       }
+       
+       /**
+        * Returns the columns of the table.
+        * 
+        * @return      IDatabaseTableColumn[]
+        */
+       public function getColumns() {
+               return $this->columns;
+       }
+       
+       /**
+        * Returns the foreign keys of the table.
+        *
+        * @return      DatabaseTableForeignKey[]
+        */
+       public function getForeignKeys() {
+               return $this->foreignKeys;
+       }
+       
+       /**
+        * Returns the indices of the table.
+        * 
+        * @return      DatabaseTableIndex[]
+        */
+       public function getIndices() {
+               return $this->indices;
+       }
+       
+       /**
+        * Returns the name of the database table.
+        * 
+        * @return      string          database table name
+        */
+       public function getName() {
+               return $this->name;
+       }
+       
+       /**
+        * Returns a `DatabaseTable` object with the given name.
+        *
+        * @param       string          $tableName
+        * @return      static
+        */
+       public static function create($tableName) {
+               return new static($tableName);
+       }
+       
+       /**
+        * Returns a `DatabaseTable` object for an existing database table with the given name.
+        * 
+        * @param       DatabaseEditor          $dbEditor
+        * @param       string                  $tableName
+        * @return      DatabaseTable
+        */
+       public static function createFromExistingTable(DatabaseEditor $dbEditor, $tableName) {
+               $table = new static($tableName);
+               
+               $columns = [];
+               foreach ($dbEditor->getColumns($tableName) as $columnData) {
+                       $className = 'wcf\system\database\table\column\\' . ucfirst(strtolower($columnData['data']['type'])) . 'DatabaseTableColumn';
+                       if (!class_exists($className)) {
+                               throw new \InvalidArgumentException("Unknown database table column type '{$columnData['data']['type']}'.");
+                       }
+                       
+                       $columns[$columnData['name']] = $className::createFromData($columnData['name'], $columnData['data']);
+               }
+               $table->columns($columns);
+               
+               $foreignKeys = [];
+               foreach ($dbEditor->getForeignKeys($tableName) as $foreignKeysName => $foreignKeyData) {
+                       $foreignKeys[$foreignKeysName] = DatabaseTableForeignKey::createFromData($foreignKeysName, $foreignKeyData);
+               }
+               $table->foreignKeys($foreignKeys);
+               
+               $indices = [];
+               foreach ($dbEditor->getIndexInformation($tableName) as $indexName => $indexData) {
+                       if (!isset($foreignKeys[$indexName])) {
+                               $indices[$indexName] = DatabaseTableIndex::createFromData($indexName, $indexData);
+                       }
+               }
+               $table->indices($indices);
+               
+               return $table;
+       }
+       
+       /**
+        * Sets the indices of the database table.
+        * 
+        * @param       DatabaseTableIndex[]    $indices        added/dropped indices
+        * @return      $this                                   this database table
+        * @throws      \InvalidArgumentException               if any index is invalid or duplicate index key names exist
+        */
+       public function indices(array $indices) {
+               $this->indices = [];
+               foreach ($indices as $index) {
+                       if (!($index instanceof DatabaseTableIndex)) {
+                               throw new \InvalidArgumentException("Added indices have to be instances of '" . DatabaseTableIndex::class . "'.");
+                       }
+                       
+                       if ($index->getName() === '') {
+                               $index->name(md5($this->getName() . '_' . $index->getColumns()[0]));
+                       }
+                       
+                       if (isset($this->foreignKeys[$index->getName()])) {
+                               throw new \InvalidArgumentException("Duplicate index with name '{$index->getName()}'.");
+                       }
+                       
+                       $this->indices[$index->getName()] = $index;
+               }
+               
+               return $this;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/DatabaseTableChangeProcessor.class.php b/wcfsetup/install/files/lib/system/database/table/DatabaseTableChangeProcessor.class.php
new file mode 100644 (file)
index 0000000..7fe862c
--- /dev/null
@@ -0,0 +1,681 @@
+<?php
+namespace wcf\system\database\table;
+use wcf\data\package\Package;
+use wcf\system\database\editor\DatabaseEditor;
+use wcf\system\database\table\column\IDatabaseTableColumn;
+use wcf\system\database\table\index\DatabaseTableForeignKey;
+use wcf\system\database\table\index\DatabaseTableIndex;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\package\SplitNodeException;
+use wcf\system\WCF;
+
+/**
+ * Processes a given set of changes to database tables.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table
+ * @since      5.2
+ */
+class DatabaseTableChangeProcessor {
+       /**
+        * added columns grouped by the table they belong to
+        * @var IDatabaseTableColumn[][]
+        */
+       protected $addedColumns = [];
+       
+       /**
+        * added indices grouped by the table they belong to
+        * @var DatabaseTableIndex[][]
+        */
+       protected $addedIndices = [];
+       
+       /**
+        * added tables
+        * @var DatabaseTable[]
+        */
+       protected $addedTables = [];
+       
+       /**
+        * maps the registered database table column names to the ids of the packages they belong to
+        * @var int[][]
+        */
+       protected $columnPackageIDs = [];
+       
+       /**
+        * 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[][]
+        */
+       protected $droppedColumns = [];
+       
+       /**
+        * dropped indices grouped by the table they belong to
+        * @var DatabaseTableIndex[][]|DatabaseTableForeignKey[][]
+        */
+       protected $droppedIndices = [];
+       
+       /**
+        * dropped tables
+        * @var DatabaseTable[]
+        */
+       protected $droppedTables = [];
+       
+       /**
+        * list of all existing tables in the used database
+        * @var string[]
+        */
+       protected $existingTableNames = [];
+       
+       /**
+        * maps the registered database table index names to the ids of the packages they belong to
+        * @var int[][]
+        */
+       protected $indexPackageIDs = [];
+       
+       /**
+        * maps the registered database table foreign key names to the ids of the packages they belong to
+        * @var int[][]
+        */
+       protected $foreignKeyPackageIDs = [];
+       
+       /**
+        * is `true` if only one change will be handled per request
+        * @var bool
+        */
+       protected $oneChangePerRequest = true;
+       
+       /**
+        * package that wants to apply the changes
+        * @var Package
+        */
+       protected $package;
+       
+       /**
+        * layouts/layout changes of the relevant database table
+        * @var DatabaseTable[]
+        */
+       protected $tables;
+       
+       /**
+        * maps the registered database table names to the ids of the packages they belong to
+        * @var int[]
+        */
+       protected $tablePackageIDs = [];
+       
+       /**
+        * Creates a new instance of `DatabaseTableChangeProcessor`.
+        * 
+        * @param       Package                 $package
+        * @param       DatabaseTable[]         $tables
+        * @param       DatabaseEditor          $dbEditor
+        * @param       bool                    $oneChangePerRequest
+        */
+       public function __construct(Package $package, array $tables, DatabaseEditor $dbEditor, $oneChangePerRequest = true) {
+               $this->package = $package;
+               
+               $tableNames = [];
+               foreach ($tables as $table) {
+                       if (!($table instanceof DatabaseTable)) {
+                               throw new \InvalidArgumentException("Tables must be instance of '" . DatabaseTable::class . "'");
+                       }
+                       
+                       $tableNames[] = $table->getName();
+               }
+               
+               $this->tables = $tables;
+               $this->dbEditor = $dbEditor;
+               $this->oneChangePerRequest = $oneChangePerRequest;
+               
+               $this->existingTableNames = $dbEditor->getTableNames();
+               
+               $conditionBuilder = new PreparedStatementConditionBuilder();
+               $conditionBuilder->add('sqlTable IN (?)', [$tableNames]);
+               
+               $sql = "SELECT  *
+                       FROM    wcf".WCF_N."_package_installation_sql_log
+                       " . $conditionBuilder;
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute($conditionBuilder->getParameters());
+               
+               while ($row = $statement->fetchArray()) {
+                       if ($row['sqlIndex'] === '' && $row['sqlColumn'] === '') {
+                               $this->tablePackageIDs[$row['sqlTable']] = $row['packageID'];
+                       }
+                       else if ($row['sqlIndex'] === '') {
+                               $this->columnPackageIDs[$row['sqlTable']][$row['sqlColumn']] = $row['packageID'];
+                       }
+                       else if (substr($row['sqlIndex'], -3) === '_fk') {
+                               $this->foreignKeyPackageIDs[$row['sqlTable']][$row['sqlIndex']] = $row['packageID'];
+                       }
+                       else {
+                               $this->indexPackageIDs[$row['sqlTable']][$row['sqlIndex']] = $row['packageID'];
+                       }
+               }
+       }
+       
+       /**
+        * Creates the given 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
+        */
+       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()}'.");
+               }
+       }
+       
+       /**
+        * 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()];
+               }
+       
+               return null;
+       }
+       
+       /**
+        * 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         $foreignKey
+        * @return      null|int
+        */
+       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()];
+               }
+               
+               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()];
+               }
+               
+               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);
+                       
+                       WCF::getDB()->beginTransaction();
+                       foreach ($this->droppedTables as $table) {
+                               $statement->execute([$table->getName()]);
+                       }
+                       WCF::getDB()->commitTransaction();
+               }
+               
+               if (!empty($this->droppedColumns)) {
+                       $sql = "DELETE FROM     wcf".WCF_N."_package_installation_sql_log
+                               WHERE           sqlTable = ?
+                                               AND sqlColumn = ?";
+                       $statement = WCF::getDB()->prepareStatement($sql);
+                       
+                       WCF::getDB()->beginTransaction();
+                       foreach ($this->droppedColumns as $tableName => $columns) {
+                               foreach ($columns as $column) {
+                                       $statement->execute([$tableName, $column->getName()]);
+                               }
+                       }
+                       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()]);
+                               }
+                       }
+                       WCF::getDB()->commitTransaction();
+               }
+               
+               $insertionData = [];
+               foreach ($this->addedTables as $table) {
+                       $insertionData[] = ['sqlTable' => $table->getName()];
+               }
+               
+               foreach ($this->addedColumns as $tableName => $columns) {
+                       foreach ($columns as $column) {
+                               $insertionData[] = ['sqlTable' => $tableName, 'sqlColumn' => $column->getName()];
+                       }
+               }
+               
+               foreach ($this->addedIndices as $tableName => $indices) {
+                       foreach ($indices as $index) {
+                               $insertionData[] = ['sqlTable' => $tableName, 'sqlIndex' => $index->getName()];
+                       }
+               }
+               
+               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'] ?? ''
+                               ]);
+                       }
+                       WCF::getDB()->commitTransaction();
+               }
+       }
+       
+       /**
+        * 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() {
+               $errors = $this->validate();
+               if (!empty($errors)) {
+                       throw new \RuntimeException(WCF::getLanguage()->getDynamicVariable('wcf.acp.package.error.databaseChange', [
+                               'errors' => $errors
+                       ]));
+               }
+               
+               foreach ($this->tables as $table) {
+                       if ($table->willBeDropped()) {
+                               if (in_array($table->getName(), $this->existingTableNames)) {
+                                       $this->dropTable($table);
+                               }
+                       }
+                       else if (!in_array($table->getName(), $this->existingTableNames)) {
+                               $this->createTable($table);
+                       }
+                       else {
+                               // calculate difference between tables
+                               $existingTable = DatabaseTable::createFromExistingTable($this->dbEditor, $table->getName());
+                               $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;
+                                       }
+                                       else if (isset($existingColumns[$column->getName()])) {
+                                               if ($column->willBeDropped()) {
+                                                       $droppedColumns[$column->getName()] = $column;
+                                               }
+                                               else if (!empty(array_diff($column->getData(), $existingColumns[$column->getName()]->getData()))) {
+                                                       $alteredColumns[$column->getName()] = $column;
+                                               }
+                                       }
+                               }
+                               
+                               $this->processColumns($table, $addedColumns, $alteredColumns, $droppedColumns);
+                               
+                               $addedForeignKeys = $droppedForeignKeys = [];
+                               foreach ($table->getForeignKeys() as $foreignKey) {
+                                       $matchingExistingForeignKey = null;
+                                       foreach ($existingForeignKeys as $existingForeignKey) {
+                                               if (empty(array_diff($foreignKey->getData(), $existingForeignKey->getData()))) {
+                                                       $matchingExistingForeignKey = $existingForeignKey;
+                                                       break;
+                                               }
+                                       }
+                                       
+                                       if ($foreignKey->willBeDropped()) {
+                                               if ($matchingExistingForeignKey !== null) {
+                                                       $droppedForeignKeys[$foreignKey->getName()] = $foreignKey;
+                                               }
+                                       }
+                                       else if ($matchingExistingForeignKey === null) {
+                                               $addedForeignKeys[$foreignKey->getName()] = $foreignKey;
+                                       }
+                               }
+                               
+                               $this->processForeignKeys($table, $addedForeignKeys, $droppedForeignKeys);
+                               
+                               $addedIndices = $droppedIndices = [];
+                               foreach ($table->getIndices() as $index) {
+                                       $matchingExistingIndex = null;
+                                       foreach ($existingIndices as $existingIndex) {
+                                               if (empty(array_diff($index->getData(), $existingIndex->getData()))) {
+                                                       $matchingExistingIndex = $existingIndex;
+                                                       break;
+                                               }
+                                       }
+                                       
+                                       if ($index->willBeDropped()) {
+                                               if ($matchingExistingIndex !== null) {
+                                                       $droppedIndices[$index->getName()] = $index;
+                                               }
+                                       }
+                                       else if ($matchingExistingIndex === null) {
+                                               $addedIndices[$index->getName()] = $index;
+                                       }
+                               }
+                               
+                               $this->processIndices($table, $addedIndices, $droppedIndices);
+                       }
+               }
+               
+               $this->logChanges();
+       }
+       
+       /**
+        * Adds, alters and drops the given columns.
+        * 
+        * @param       DatabaseTable                   $table
+        * @param       IDatabaseTableColumn[]          $addedColumns
+        * @param       IDatabaseTableColumn[]          $alteredColumns
+        * @param       IDatabaseTableColumn[]          $droppedColumns
+        * @throws      SplitNodeException
+        */
+       protected function processColumns(DatabaseTable $table, array $addedColumns, array $alteredColumns, array $droppedColumns) {
+               $columnData = [];
+               foreach ($droppedColumns as $droppedColumn) {
+                       $columnData[$droppedColumn->getName()] = [
+                               'action' => 'drop'
+                       ];
+                       
+                       $this->droppedColumns[$table->getName()][$droppedColumn->getName()] = $droppedColumn;
+               }
+               foreach ($addedColumns as $addedColumn) {
+                       $columnData[$addedColumn->getName()] = [
+                               'action' => 'add',
+                               'data' => $addedColumn->getData()
+                       ];
+                       
+                       if ($this->tablePackageIDs[$table->getName()] !== $this->package->packageID) {
+                               $this->addedColumns[$table->getName()][$addedColumn->getName()] = $addedColumn;
+                       }
+               }
+               foreach ($alteredColumns as $alteredColumn) {
+                       $columnData[$alteredColumn->getName()] = [
+                               'action' => 'alter',
+                               'data' => $alteredColumn->getData(),
+                               'oldColumnName' => $alteredColumn->getName()
+                       ];
+               }
+               
+               if (!empty($columnData)) {
+                       $this->dbEditor->alterColumns($table->getName(), $columnData);
+                       
+                       if ($this->oneChangePerRequest) {
+                               $this->logChanges();
+                               
+                               throw new SplitNodeException("Altered columns of table '{$table->getName()}'.");
+                       }
+               }
+       }
+       
+       /**
+        * Adds and drops the given foreign keys.
+        * 
+        * @param       DatabaseTable                   $table
+        * @param       DatabaseTableForeignKey[]       $addedForeignKeys
+        * @param       DatabaseTableForeignKey[]       $droppedForeignKeys
+        * @throws      SplitNodeException
+        */
+       protected function processForeignKeys(DatabaseTable $table, array $addedForeignKeys, array $droppedForeignKeys) {
+               if (empty($addedForeignKeys) && empty($droppedForeignKeys)) {
+                       return;
+               }
+               
+               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()) . "'");
+                       }
+               }
+               
+               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()})");
+                       }
+               }
+       }
+       
+       /**
+        * Adds and drops the given indices.
+        * 
+        * @param       DatabaseTable           $table
+        * @param       DatabaseTableIndex[]    $addedIndices
+        * @param       DatabaseTableIndex[]    $droppedIndices
+        * @throws      SplitNodeException
+        */
+       protected function processIndices(DatabaseTable $table, array $addedIndices, array $droppedIndices) {
+               if (empty($addedIndices) && empty($droppedIndices)) {
+                       return;
+               }
+               
+               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()) . "'");
+                       }
+               }
+               
+               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()) . "'");
+                       }
+               }
+       }
+       
+       /**
+        * Checks if the relevant table layout changes can be executed and returns an array with information
+        * on any validation error.
+        * 
+        * @return      array
+        */
+       public function validate() {
+               $errors = [];
+               foreach ($this->tables as $table) {
+                       if ($table->willBeDropped()) {
+                               if (in_array($table->getName(), $this->existingTableNames)) {
+                                       if (!isset($this->tablePackageIDs[$table->getName()])) {
+                                               $errors[] = [
+                                                       'tableName' => $table->getName(),
+                                                       'type' => 'unregisteredTableDrop'
+                                               ];
+                                       }
+                                       else if ($this->tablePackageIDs[$table->getName()] !== $this->package->packageID) {
+                                               $errors[] = [
+                                                       'tableName' => $table->getName(),
+                                                       'type' => 'foreignTableDrop'
+                                               ];
+                                       }
+                               }
+                       }
+                       else if (in_array($table->getName(), $this->existingTableNames)) {
+                               if (!isset($this->tablePackageIDs[$table->getName()])) {
+                                       $errors[] = [
+                                               'tableName' => $table->getName(),
+                                               'type' => 'unregisteredTableChange'
+                                       ];
+                               }
+                               else {
+                                       $existingTable = DatabaseTable::createFromExistingTable($this->dbEditor, $table->getName());
+                                       $existingColumns = $existingTable->getColumns();
+                                       $existingIndices = $existingTable->getIndices();
+                                       $existingForeignKeys = $existingTable->getForeignKeys();
+                                       
+                                       foreach ($table->getColumns() as $column) {
+                                               if (isset($existingColumns[$column->getName()])) {
+                                                       $columnPackageID = $this->getColumnPackageID($table, $column);
+                                                       if ($column->willBeDropped()) {
+                                                               if ($columnPackageID !== $this->package->packageID) {
+                                                                       $errors[] = [
+                                                                               'columnName' => $column->getName(),
+                                                                               'tableName' => $table->getName(),
+                                                                               'type' => 'foreignColumnDrop'
+                                                                       ];
+                                                               }
+                                                       }
+                                                       else if ($columnPackageID !== $this->package->packageID) {
+                                                               $errors[] = [
+                                                                       'columnName' => $column->getName(),
+                                                                       'tableName' => $table->getName(),
+                                                                       'type' => 'foreignColumnChange'
+                                                               ];
+                                                       }
+                                               }
+                                       }
+                                       
+                                       foreach ($table->getIndices() as $index) {
+                                               foreach ($existingIndices as $existingIndex) {
+                                                       if (empty(array_diff($index->getData(), $existingIndex->getData()))) {
+                                                               if ($index->willBeDropped()) {
+                                                                       if ($this->getIndexPackageID($table, $index) !== $this->package->packageID) {
+                                                                               $errors[] = [
+                                                                                       'columnNames' => implode(',', $existingIndex->getColumns()),
+                                                                                       'tableName' => $table->getName(),
+                                                                                       'type' => 'foreignIndexDrop'
+                                                                               ];
+                                                                       }
+                                                               }
+                                                               
+                                                               continue 2;
+                                                       }
+                                               }
+                                       }
+                                       
+                                       foreach ($table->getForeignKeys() as $foreignKey) {
+                                               foreach ($existingForeignKeys as $existingForeignKey) {
+                                                       if (empty(array_diff($foreignKey->getData(), $existingForeignKey->getData()))) {
+                                                               if ($foreignKey->willBeDropped()) {
+                                                                       if ($this->getForeignKeyPackageID($table, $foreignKey) !== $this->package->packageID) {
+                                                                               $errors[] = [
+                                                                                       'columnNames' => implode(',', $existingForeignKey->getColumns()),
+                                                                                       'tableName' => $table->getName(),
+                                                                                       'type' => 'foreignForeignKeyDrop'
+                                                                               ];
+                                                                       }
+                                                               }
+                                                               
+                                                               continue 2;
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               }
+               
+               return $errors;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/TDroppableDatabaseComponent.class.php b/wcfsetup/install/files/lib/system/database/table/TDroppableDatabaseComponent.class.php
new file mode 100644 (file)
index 0000000..9274149
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+namespace wcf\system\database\table;
+
+/**
+ * Provides methods for database components which can be dropped.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table
+ * @since      5.2
+ */
+trait TDroppableDatabaseComponent {
+       /**
+        * is `true` if the component will be dropped
+        * @var bool
+        */
+       protected $drop = false;
+       
+       /**
+        * Marks the component to be dropped.
+        * 
+        * @return      $this
+        */
+       public function drop() {
+               $this->drop = true;
+               
+               return $this;
+       }
+       
+       /**
+        * Returns `true` if the component will be dropped.
+        * 
+        * @return      bool
+        */
+       public function willBeDropped() {
+               return $this->drop;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/AbstractDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/AbstractDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..3952e49
--- /dev/null
@@ -0,0 +1,203 @@
+<?php
+namespace wcf\system\database\table\column;
+use wcf\system\database\table\TDroppableDatabaseComponent;
+
+/**
+ * Abstract implementation of a database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+abstract class AbstractDatabaseTableColumn implements IDatabaseTableColumn {
+       use TDroppableDatabaseComponent;
+       
+       /**
+        * default value of the database table column
+        * @var mixed
+        */
+       protected $defaultValue;
+       
+       /**
+        * name of the database table column
+        * @var string
+        */
+       protected $name;
+       
+       /**
+        * is `true` if the values of the column may not be `null`
+        * @var bool
+        */
+       protected $notNull = false;
+       
+       /**
+        * type of the database table column
+        * @var string
+        */
+       protected $type;
+       
+       /**
+        * @inheritDoc
+        */
+       public function defaultValue($defaultValue) {
+               $this->validateDefaultValue($defaultValue);
+               
+               $this->defaultValue = $defaultValue;
+               
+               return $this;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getData() {
+               $data = [
+                       'default' => $this->defaultValue,
+                       'notNull' => $this->isNotNull() ? 1 : 0,
+                       'type' => $this->getType()
+               ];
+               
+               if ($this instanceof IAutoIncrementDatabaseTableColumn) {
+                       $data['autoIncrement'] = $this->isAutoIncremented() ? 1 : 0;
+               }
+               
+               if ($this instanceof IDecimalsDatabaseTableColumn && $this->getDecimals() !== null) {
+                       $data['decimals'] = $this->getDecimals();
+               }
+               
+               if ($this instanceof IEnumDatabaseTableColumn) {
+                       $values = array_map(function($value) {
+                               return str_replace(["'", '\\'], ["''", '\\\\'], $value);
+                       }, $this->getEnumValues());
+                       
+                       $data['values'] = "'" . implode("','", $values) . "'";
+               }
+               
+               if ($this instanceof ILengthDatabaseTableColumn) {
+                       $data['length'] = $this->getLength();
+               }
+               
+               if ($this instanceof IUnsignedDatabaseTableColumn) {
+                       $data['unsigned'] = $this->isUnsigned();
+               }
+               
+               return $data;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getDefaultValue() {
+               return $this->defaultValue;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getName() {
+               if ($this->name === null) {
+                       throw new \BadMethodCallException("Name of the database table column has not been set yet");
+               }
+               
+               return $this->name;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getType() {
+               if ($this->type === null) {
+                       throw new \BadMethodCallException("Type of the database table column " . get_class($this) . " has not been set yet");
+               }
+               
+               return $this->type;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function isNotNull() {
+               return $this->notNull;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function name($name) {
+               if ($this->name !== null) {
+                       throw new \BadMethodCallException("Name of the database table column has already been set.");
+               }
+               
+               $this->name = $name;
+               
+               return $this;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function notNull($notNull = true) {
+               $this->notNull = $notNull;
+               
+               return $this;
+       }
+       
+       /**
+        * Checks if the given default value is valid.
+        * 
+        * @param       mixed           $defaultValue   validated default value
+        * @throws      \InvalidArgumentException       if given default value is invalid
+        */
+       protected function validateDefaultValue($defaultValue) {
+               // does nothing
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function create($name) {
+               return (new static())->name($name);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function createFromData($name, array $data) {
+               $column = static::create($name)
+                       ->defaultValue($data['default'])
+                       ->notNull($data['notNull']);
+               
+               if ($column instanceof IAutoIncrementDatabaseTableColumn) {
+                       $column->autoIncrement($data['autoIncrement'] ?: null);
+               }
+               
+               if ($column instanceof IDecimalsDatabaseTableColumn) {
+                       $column->decimals($data['decimals'] ?: null);
+               }
+               
+               if ($column instanceof IEnumDatabaseTableColumn) {
+                       $values = explode(',', $data['enumValues'] ?? []);
+                       
+                       $values = array_map(function($value) {
+                               // trim one leading and one trailing `'`
+                               $value = substr($value, 1, -1);
+                               
+                               return str_replace(['\\\\', "''"], ['\\', "'"], $value);
+                       }, $values);
+                       
+                       $column->enumValues($values);
+               }
+               
+               if ($column instanceof ILengthDatabaseTableColumn) {
+                       $column->length($data['length'] ?: null);
+               }
+               
+               if ($column instanceof IUnsignedDatabaseTableColumn) {
+                       $column->unsigned($data['unsigned'] ?? false);
+               }
+               
+               return $column;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/AbstractDecimalDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/AbstractDecimalDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..e76adce
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Abstract implementation of a decimal (data) type for database table columns.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+abstract class AbstractDecimalDatabaseTableColumn extends AbstractDatabaseTableColumn implements IDecimalsDatabaseTableColumn, IUnsignedDatabaseTableColumn {
+       use TDecimalsDatabaseTableColumn;
+       use TUnsignedDatabaseTableColumn;
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/AbstractIntDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/AbstractIntDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..9e01837
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Abstract implementation of an integer database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+abstract class AbstractIntDatabaseTableColumn extends AbstractDatabaseTableColumn implements IAutoIncrementDatabaseTableColumn, ILengthDatabaseTableColumn, IUnsignedDatabaseTableColumn {
+       use TAutoIncrementDatabaseTableColumn;
+       use TLengthDatabaseTableColumn;
+       use TUnsignedDatabaseTableColumn;
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMinimumLength() {
+               return 1;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/BigintDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/BigintDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..0340420
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `bigint` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class BigintDatabaseTableColumn extends AbstractIntDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'bigint';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumLength() {
+               return 20;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumUnsignedLength() {
+               return 19;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/BinaryDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/BinaryDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..4b1189a
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `binary` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class BinaryDatabaseTableColumn extends AbstractDatabaseTableColumn implements ILengthDatabaseTableColumn {
+       use TLengthDatabaseTableColumn;
+       
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'binary';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumLength() {
+               return 255;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMinimumLength() {
+               return 0;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/BlobDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/BlobDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..a6335b7
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `blob` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class BlobDatabaseTableColumn extends AbstractDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'blob';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/CharDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/CharDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..14b4ba7
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `char` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class CharDatabaseTableColumn extends AbstractDatabaseTableColumn implements ILengthDatabaseTableColumn {
+       use TLengthDatabaseTableColumn;
+       
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'char';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumLength() {
+               return 255;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMinimumLength() {
+               return 0;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/DateDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/DateDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..6a5d54c
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `date` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class DateDatabaseTableColumn extends AbstractDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'date';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/DatetimeDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/DatetimeDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..d4df746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `datetime` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class DatetimeDatabaseTableColumn extends AbstractDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'datetime';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/DecimalDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/DecimalDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..6a4f408
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `decimal` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class DecimalDatabaseTableColumn extends AbstractDecimalDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'decimal';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumDecimals() {
+               return 30;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumLength() {
+               return 65;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMinimumLength() {
+               return 1;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/DefaultFalseBooleanDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/DefaultFalseBooleanDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..b263324
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `tinyint` database table column with length `1`, default value `0` and whose values
+ * cannot be `null`.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class DefaultFalseBooleanDatabaseTableColumn extends TinyintDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       public static function create($name) {
+               /** @var TinyintDatabaseTableColumn $column */
+               $column = parent::create($name);
+               
+               return $column
+                       ->length(1)
+                       ->notNull()
+                       ->defaultValue(0);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/DefaultTrueBooleanDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/DefaultTrueBooleanDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..f5a79bc
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `tinyint` database table column with length `1`, default value `1` and whose values
+ * cannot be `null`.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class DefaultTrueBooleanDatabaseTableColumn extends TinyintDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       public static function create($name) {
+               /** @var TinyintDatabaseTableColumn $column */
+               $column = parent::create($name);
+               
+               return $column
+                       ->length(1)
+                       ->notNull()
+                       ->defaultValue(1);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/DoubleDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/DoubleDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..b9d47cc
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `double` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class DoubleDatabaseTableColumn extends AbstractDecimalDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'double';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumDecimals() {
+               return 30;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumLength() {
+               return 255;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/EnumDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/EnumDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..44cf11c
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `enum` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class EnumDatabaseTableColumn extends AbstractDatabaseTableColumn implements IEnumDatabaseTableColumn {
+       use TEnumDatabaseTableColumn;
+       
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'enum';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/FloatDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/FloatDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..1783c71
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `float` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class FloatDatabaseTableColumn extends AbstractDecimalDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'float';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumDecimals() {
+               return 30;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumLength() {
+               return 255;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/IAutoIncrementDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/IAutoIncrementDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..e3dd0b1
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Every database table column whose values can be auto-incremented must implement this interface.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+interface IAutoIncrementDatabaseTableColumn {
+       /**
+        * Sets if the values of the database table column are auto-increment and returns this column.
+        * 
+        * @param       bool    $autoIncrement
+        * @return      $this
+        */
+       public function autoIncrement($autoIncrement = true);
+       
+       /**
+        * Returns `true` if the values of the database table column are auto-increment.
+        * 
+        * @return      bool
+        */
+       public function isAutoIncremented();
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/IDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/IDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..d9f164d
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a column of a database table.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+interface IDatabaseTableColumn {
+       /**
+        * Sets the default value of the column and returns the column.
+        * 
+        * @param       mixed   $defaultValue
+        * @return      $this
+        */
+       public function defaultValue($defaultValue);
+
+       /**
+        * Marks the column to be dropped and returns the column.
+        * 
+        * @return      $this
+        */
+       public function drop();
+       
+       /**
+        * Returns the data used by `DatabaseEditor` to add the column to a table.
+        *
+        * @return      array
+        */
+       public function getData();
+       
+       /**
+        * Returns the default value of the column.
+        * 
+        * @return      $this
+        */
+       public function getDefaultValue();
+       
+       /**
+        * Returns the name of the column.
+        * 
+        * @return      string
+        */
+       public function getName();
+       
+       /**
+        * Returns the type of the column.
+        * 
+        * @return      string
+        */
+       public function getType();
+       
+       /**
+        * Returns `true` if the values of the column cannot be `null`.
+        * 
+        * @return      bool
+        */
+       public function isNotNull();
+       
+       /**
+        * Sets the name of the column and returns the column.
+        * 
+        * @param       string          $name
+        * @return      $this
+        */
+       public function name($name);
+       
+       /**
+        * Sets if the values of the column cannot be `null`.
+        * 
+        * @param       bool    $notNull
+        * @return      $this
+        */
+       public function notNull($notNull = true);
+       
+       /**
+        * Returns `true` if the column will be dropped.
+        *
+        * @return      bool
+        */
+       public function willBeDropped();
+       
+       /**
+        * Returns a `DatabaseTableColumn` object with the given name.
+        * 
+        * @param       string          $name
+        * @return      $this
+        */
+       public static function create($name);
+
+       /**
+        * Returns a `DatabaseTableColumn` object with the given name and data.
+        * 
+        * @param       string          $name
+        * @param       array           $data           data returned by `DatabaseEditor::getColumns()`
+        * @return      $this
+        */
+       public static function createFromData($name, array $data);
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/IDecimalsDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/IDecimalsDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..3fa6c39
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Every database table column whose values supports decimals must implement this interface.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+interface IDecimalsDatabaseTableColumn extends ILengthDatabaseTableColumn {
+       /**
+        * Sets the number of decimals the database table column supports or unsets the previously
+        * set value if `null` is passed and returns this column.
+        * 
+        * @param       null|int        $decimals
+        * @return      $this
+        */
+       public function decimals($decimals);
+       
+       /**
+        * Returns the number of decimals the database table column supports or `null` if the number
+        * of decimals has not be specified.
+        * 
+        * @return      null|int
+        */
+       public function getDecimals();
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/IEnumDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/IEnumDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..936008c
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Every database table column that supports specifying a predetermined set of valid values must
+ * implement this interface.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+interface IEnumDatabaseTableColumn extends IDatabaseTableColumn {
+       /**
+        * Sets the predetermined set of valid values for the database table column and returns this
+        * column.
+        * 
+        * @param       array           $values
+        * @return      $this
+        */
+       public function enumValues(array $values);
+       
+       /**
+        * Returns the predetermined set of valid values for the database table column.
+        * 
+        * @return      array
+        */
+       public function getEnumValues();
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/ILengthDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/ILengthDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..63f4490
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Every database table column that supports specifying a (maximum) value length must implement this
+ * interface.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+interface ILengthDatabaseTableColumn extends IDatabaseTableColumn {
+       /**
+        * Returns the (maximum) length of the column's values or `null` if no length has been set.
+        * 
+        * @return      null|int
+        */
+       public function getLength();
+       
+       /**
+        * Sets the (maximum) length of the column's values.
+        * 
+        * @param       null|int        $length         (maximum) column value length or `null` to unset previously set value
+        * @return      $this                           this column
+        * @throws      \InvalidArgumentException       if given length is invalid
+        */
+       public function length($length);
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/IUnsignedDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/IUnsignedDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..b41ae3e
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Every database table column whose values can be unsigned must implement this interface.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+interface IUnsignedDatabaseTableColumn extends IDatabaseTableColumn {
+       /**
+        * Returns `true` if the values of the database table column are unsigned.
+        *
+        * @return      bool
+        */
+       public function isUnsigned();
+       
+       /**
+        * Sets if the values of the database table column are unsigned and returns this column.
+        *
+        * @param       bool    $unsigned
+        * @return      $this
+        */
+       public function unsigned($unsigned = true);
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/IntDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/IntDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..8a3ab91
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `int` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class IntDatabaseTableColumn extends AbstractIntDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'int';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumLength() {
+               return 11;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/LongblobDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/LongblobDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..2d515fc
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `longblob` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class LongblobDatabaseTableColumn extends AbstractDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'longblob';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/LongtextDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/LongtextDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..49a5cb7
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `longtext` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class LongtextDatabaseTableColumn extends AbstractDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'longtext';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/MediumblobDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/MediumblobDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..8c104a8
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `mediumblob` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class MediumblobDatabaseTableColumn extends AbstractDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'mediumblob';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/MediumintDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/MediumintDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..4259778
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `mediumint` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class MediumintDatabaseTableColumn extends AbstractIntDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'mediumint';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumLength() {
+               return 8;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumUnsignedLength() {
+               return 7;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/MediumtextDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/MediumtextDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..3dea632
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `mediumtext` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class MediumtextDatabaseTableColumn extends AbstractDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'mediumtext';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/NotNullInt10DatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/NotNullInt10DatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..c083be9
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `int` database table column with length `10` and whose values cannot be null.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class NotNullInt10DatabaseTableColumn extends IntDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       public static function create($name) {
+               return parent::create($name)
+                       ->notNull()
+                       ->length(10);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/NotNullVarchar191DatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/NotNullVarchar191DatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..4cd2014
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `varchar` database table column with length `191` and whose values cannot be null.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class NotNullVarchar191DatabaseTableColumn extends VarcharDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       public static function create($name) {
+               return parent::create($name)
+                       ->notNull()
+                       ->length(191);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/NotNullVarchar255DatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/NotNullVarchar255DatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..91eef6c
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `varchar` database table column with length `255` and whose values cannot be null.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class NotNullVarchar255DatabaseTableColumn extends VarcharDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       public static function create($name) {
+               return parent::create($name)
+                       ->notNull()
+                       ->length(255);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/ObjectIdDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/ObjectIdDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..0224def
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `int` database table column with length `10`, whose values cannot be null, and whose
+ * values are auto-incremented.
+ * 
+ * This class should be used for the id column of DBO tables.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class ObjectIdDatabaseTableColumn extends NotNullInt10DatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       public static function create($columnName) {
+               return parent::create($columnName)
+                       ->autoIncrement();
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/SetDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/SetDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..f050eca
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `set` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class SetDatabaseTableColumn extends AbstractDatabaseTableColumn implements IEnumDatabaseTableColumn {
+       use TEnumDatabaseTableColumn;
+       
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'set';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/SmallintDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/SmallintDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..b138765
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `smallint` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class SmallintDatabaseTableColumn extends AbstractIntDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'smallint';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumLength() {
+               return 5;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/TAutoIncrementDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/TAutoIncrementDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..7e5a548
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Provides default implementation of the methods of `IAutoIncrementDatabaseTableColumn`.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+trait TAutoIncrementDatabaseTableColumn {
+       /**
+        * is `true` if the values of the database table column are auto-increment
+        * @var bool
+        */
+       protected $autoIncrement = false;
+       
+       /**
+        * Sets if the values of the database table column are auto-increment and returns this column.
+        *
+        * @param       bool    $autoIncrement
+        * @return      $this
+        */
+       public function autoIncrement($autoIncrement = true) {
+               $this->autoIncrement = $autoIncrement;
+               
+               return $this;
+       }
+       
+       /**
+        * Returns `true` if the values of the database table column are auto-increment.
+        *
+        * @return      bool
+        */
+       public function isAutoIncremented() {
+               return $this->autoIncrement;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/TDecimalsDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/TDecimalsDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..a1c6ace
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Provides default implementation of the methods of `IDecimalsDatabaseTableColumn`.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+trait TDecimalsDatabaseTableColumn {
+       use TLengthDatabaseTableColumn;
+       
+       /**
+        * number of decimals the database table column supports
+        * @var null|int
+        */
+       protected $decimals;
+       
+       /**
+        * Sets the number of decimals the database table column supports or unsets the previously
+        * set value if `null` is passed and returns this column.
+        *
+        * @param       null|int        $decimals
+        * @return      $this
+        */
+       public function decimals($decimals) {
+               if ($this->getMaximumDecimals() !== null && $decimals > $this->getMaximumDecimals()) {
+                       throw new \InvalidArgumentException("Given number of decimals is greater than the maximum number '{$this->getMaximumDecimals()}'.");
+               }
+               
+               $this->decimals = $decimals;
+               
+               return $this;
+       }
+
+       /**
+        * Returns the number of decimals the database table column supports or `null` if the number
+        * of decimals has not be specified.
+        *
+        * @return      null|int
+        */
+       public function getDecimals() {
+               return $this->decimals;
+       }
+       
+       /**
+        * Returns the maxium number of decimals supported by this column or `null` if there is no such
+        * maximum.
+        *
+        * @return      null|int
+        */
+       public function getMaximumDecimals() {
+               return null;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/TEnumDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/TEnumDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..69f1586
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Provides default implementation of the methods of `IEnumDatabaseTableColumn`.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+trait TEnumDatabaseTableColumn {
+       /**
+        * predetermined set of valid values for the database table column
+        * @var array
+        */
+       protected $enumValues = [];
+       
+       /**
+        * Sets the predetermined set of valid values for the database table column and returns this
+        * column.
+        *
+        * @param       array           $values
+        * @return      $this
+        */
+       public function enumValues(array $values) {
+               $this->enumValues = $values;
+               
+               return $this;
+       }
+       
+       /**
+        * Returns the predetermined set of valid values for the database table column.
+        *
+        * @return      array
+        */
+       public function getEnumValues() {
+               return $this->enumValues;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/TLengthDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/TLengthDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..582a97a
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Provides default implementation of the methods of `ILengthDatabaseTableColumn`.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+trait TLengthDatabaseTableColumn {
+       /**
+        * (maximum) length of the column's values
+        * @var null|int
+        */
+       protected $length;
+       
+       /**
+        * Returns the maxium length value supported by this column or `null` if there is no such
+        * maximum.
+        *
+        * @return      null|int
+        */
+       public function getMaximumLength() {
+               if ($this instanceof IUnsignedDatabaseTableColumn && $this->getMaximumUnsignedLength() !== null) {
+                       return $this->getMaximumUnsignedLength();
+               }
+               
+               return null;
+       }
+       
+       /**
+        * Returns the maximum length value supported by the values of the column are unsigned values
+        * or `null` if there is no such maximum.
+        * 
+        * @return      null|int
+        */
+       public function getMaximumUnsignedLength() {
+               return null;
+       }
+       
+       /**
+        * Returns the minimum length value supported by this column or `null` if there is no such
+        * minimum.
+        * 
+        * @return      null|int
+        */
+       public function getMinimumLength() {
+               return 0;
+       }
+       
+       /**
+        * Returns the (maximum) length of the column's values or `null` if no length has been set.
+        * 
+        * @return      null|int
+        */
+       public function getLength() {
+               return $this->length;
+       }
+       
+       /**
+        * Sets the (maximum) length of the column's values.
+        * 
+        * @param       null|int        $length         (maximum) column value length or `null` to unset previously set value
+        * @return      $this                           this column
+        * @throws      \InvalidArgumentException       if given length is invalid
+        */
+       public function length($length) {
+               if ($length !== null) {
+                       $this->validateLength($length);
+               }
+               
+               $this->length = $length;
+               
+               return $this;
+       }
+       
+       /**
+        * Validates the given length.
+        * 
+        * @param       int     $length
+        * @throws      \InvalidArgumentException       if given length is invalid
+        */
+       protected function validateLength($length) {
+               if ($this->getMinimumLength() !== null && $length < $this->getMinimumLength()) {
+                       throw new \InvalidArgumentException("Given length is smaller than the minimum length '{$this->getMinimumLength()}'.");
+               }
+               if ($this->getMaximumLength() !== null && $length > $this->getMaximumLength()) {
+                       throw new \InvalidArgumentException("Given length is greater than the maximum length '{$this->getMaximumLength()}'.");
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/TUnsignedDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/TUnsignedDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..aa6ef31
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Provides default implementation of the methods of `IUnsignedDatabaseTableColumn`.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+trait TUnsignedDatabaseTableColumn {
+       /**
+        * `true` if the values of the database table column are unsigned
+        * @var bool
+        */
+       protected $unsigned = false;
+       
+       /**
+        * Returns `true` if the values of the database table column are unsigned.
+        * 
+        * @return      bool
+        */
+       public function isUnsigned() {
+               return $this->unsigned;
+       }
+       
+       /**
+        * Sets if the values of the database table column are unsigned and returns this column.
+        * 
+        * @param       bool    $unsigned
+        * @return      $this
+        */
+       public function unsigned($unsigned = true) {
+               $this->unsigned = $unsigned;
+               
+               return $this;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/TextDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/TextDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..0293201
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `text` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class TextDatabaseTableColumn extends AbstractDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'text';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/TimeDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/TimeDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..82e7f9d
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `time` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class TimeDatabaseTableColumn extends AbstractDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'time';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/TinyblobDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/TinyblobDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..c8b7e5c
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `tinyblob` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class TinyblobDatabaseTableColumn extends AbstractDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'tinyblob';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/TinyintDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/TinyintDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..a493e27
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `tinyint` database table column.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class TinyintDatabaseTableColumn extends AbstractIntDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'tinyint';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumLength() {
+               return 3;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/TinytextDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/TinytextDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..45d796b
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `tinytext` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class TinytextDatabaseTableColumn extends AbstractDatabaseTableColumn {
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'tinytext';
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/VarbinaryDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/VarbinaryDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..dc32087
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `varbinary` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class VarbinaryDatabaseTableColumn extends AbstractDatabaseTableColumn implements ILengthDatabaseTableColumn {
+       use TLengthDatabaseTableColumn;
+       
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'varbinary';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumLength() {
+               return 65535;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/VarcharDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/VarcharDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..58d6bf2
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `varchar` database table column.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class VarcharDatabaseTableColumn extends AbstractDatabaseTableColumn implements ILengthDatabaseTableColumn {
+       use TLengthDatabaseTableColumn;
+       
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'varchar';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMaximumLength() {
+               return 65535;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getMinimumLength() {
+               return 0;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/column/YearDatabaseTableColumn.class.php b/wcfsetup/install/files/lib/system/database/table/column/YearDatabaseTableColumn.class.php
new file mode 100644 (file)
index 0000000..e3cb51d
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+namespace wcf\system\database\table\column;
+
+/**
+ * Represents a `year` database table column.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Column
+ * @since      5.2
+ */
+class YearDatabaseTableColumn extends AbstractDatabaseTableColumn implements ILengthDatabaseTableColumn {
+       use TLengthDatabaseTableColumn;
+       
+       /**
+        * @inheritDoc
+        */
+       protected $type = 'year';
+       
+       /**
+        * @inheritDoc
+        */
+       protected function validateLength($length) {
+               if ($length !== 2 && $length !== 4) {
+                       throw new \InvalidArgumentException("Only '2' and '4' are valid lengths for year columns");
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/index/DatabaseTableForeignKey.class.php b/wcfsetup/install/files/lib/system/database/table/index/DatabaseTableForeignKey.class.php
new file mode 100644 (file)
index 0000000..65d332a
--- /dev/null
@@ -0,0 +1,271 @@
+<?php
+namespace wcf\system\database\table\index;
+use wcf\system\application\ApplicationHandler;
+use wcf\system\database\table\TDroppableDatabaseComponent;
+
+/**
+ * Represents a foreign key of a database table.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Index
+ * @since      5.2
+ */
+class DatabaseTableForeignKey {
+       use TDroppableDatabaseComponent;
+       
+       /**
+        * columns affected by the foreign key 
+        * @var string[]
+        */
+       protected $columns;
+       
+       /**
+        * name of the foreign key
+        * @var string
+        */
+       protected $name;
+       
+       /**
+        * action executed in referenced table if row is deleted
+        * @var null|string
+        */
+       protected $onDelete;
+       
+       /**
+        * action executed in referenced table if row is updated
+        * @var null|string
+        */
+       protected $onUpdate;
+       
+       /**
+        * relevant columns in referenced table
+        * @var string[]
+        */
+       protected $referencedColumns;
+       
+       /**
+        * name of referenced table
+        * @var string
+        */
+       protected $referencedTable;
+       
+       /**
+        * valid on delete/update actions
+        * @var string[]
+        */
+       const VALID_ACTIONS = [
+               'CASCADE',
+               'NO ACTION',
+               'SET NULL'
+       ];
+       
+       /**
+        * Creates a new `DatabaseTableForeignKey` object.
+        *
+        * @param       string          $name           column name
+        */
+       protected function __construct($name) {
+               $this->name = $name;
+       }
+       
+       /**
+        * Sets the columns affected by the foreign key and returns the foreign key.
+        * 
+        * @param       string[]        $columns        columns affected by foreign key
+        * @return      $this                           this foreign key
+        */
+       public function columns(array $columns) {
+               $this->columns = array_values($columns);
+               
+               return $this;
+       }
+       
+       /**
+        * Returns the name of the foreign key.
+        * 
+        * If the key belongs to a database table layout not created from an existing database table,
+        * the name might be empty.
+        * 
+        * @return      string
+        */
+       public function getName() {
+               return $this->name;
+       }
+       
+       /**
+        * Returns the columns affected by the foreign key
+        * 
+        * @return      string[]                        columns affected by foreign key
+        * @throws      \BadMethodCallException         if not columns have been set
+        */
+       public function getColumns() {
+               if ($this->columns === null) {
+                       throw new \BadMethodCallException("Before getting the columns, they must be set for foreign key '{$this->getName()}'.");
+               }
+               
+               return $this->columns;
+       }
+       
+       /**
+        * Returns the data used by `DatabaseEditor` to add the foreign key to a table.
+        * 
+        * @return      array
+        */
+       public function getData() {
+               return [
+                       'columns' => implode(',', $this->getColumns()),
+                       'ON DELETE' => $this->getOnDelete(),
+                       'ON UPDATE' => $this->getOnUpdate(),
+                       'referencedColumns' => implode(',', $this->getReferencedColumns()),
+                       'referencedTable' => $this->getReferencedTable()
+               ];
+       }
+       
+       /**
+        * Returns the action executed in referenced table if row is deleted or `null` if no such
+        * action has been set.
+        * 
+        * @return      null|string
+        */
+       public function getOnDelete() {
+               return $this->onDelete;
+       }
+       
+       /**
+        * Returns the action executed in referenced table if row is updated or `null` if no such
+        * action has been set.
+        * 
+        * @return      null|string
+        */
+       public function getOnUpdate() {
+               return $this->onUpdate;
+       }
+       
+       /**
+        * Returns the relevant columns in referenced table.
+        * 
+        * @return      string[]
+        * @throws      \BadMethodCallException         if referenced columns have not been set
+        */
+       public function getReferencedColumns() {
+               if ($this->referencedColumns === null) {
+                       throw new \BadMethodCallException("Before getting the referenced columns, they must be set for foreign key '{$this->getName()}'.");
+               }
+               
+               return $this->referencedColumns;
+       }
+       
+       /**
+        * Returns the name of the referenced table.
+        * 
+        * @return      string
+        * @throws      \BadMethodCallException         if referenced table has not been set
+        */
+       public function getReferencedTable() {
+               if ($this->referencedTable === null) {
+                       throw new \BadMethodCallException("Before getting the referenced table, it must be set for foreign key '{$this->getName()}'.");
+               }
+               
+               return $this->referencedTable;
+       }
+       
+       /**
+        * Sets the name of the foreign key.
+        *
+        * @param       string          $name           index name
+        * @return      $this                           this index
+        */
+       public function name($name) {
+               $this->name = $name;
+               
+               return $this;
+       }
+       
+       /**
+        * Sets the action executed in referenced table if row is deleted and returns the foreign
+        * key.
+        * 
+        * @param       string          $onDelete       action executed in referenced table if row is deleted
+        * @return      $this                           this foreign key
+        * @throws      \InvalidArgumentException       if given action is invalid
+        */
+       public function onDelete($onDelete) {
+               if ($onDelete !== null && !in_array($onDelete, static::VALID_ACTIONS)) {
+                       throw new \InvalidArgumentException("Unknown on delete action '{$onDelete}'.");
+               }
+               
+               $this->onDelete = $onDelete;
+               
+               return $this;
+       }
+       
+       /**
+        * Sets the action executed in referenced table if row is updated and returns the foreign
+        * key.
+        * 
+        * @param       string          $onUpdate       action executed in referenced table if row is updated
+        * @return      $this                           this foreign key
+        * @throws      \InvalidArgumentException       if given action is invalid
+        */
+       public function onUpdate($onUpdate) {
+               if ($onUpdate !== null && !in_array($onUpdate, static::VALID_ACTIONS)) {
+                       throw new \InvalidArgumentException("Unknown on update action '{$onUpdate}'.");
+               }
+               
+               $this->onUpdate = $onUpdate;
+               
+               return $this;
+       }
+       
+       /**
+        * Sets the relevant columns of the referenced table and returns the foreign key.
+        * 
+        * @param       string[]        $referencedColumns      columns of referenced table
+        * @return      $this                                   this foreign key
+        */
+       public function referencedColumns(array $referencedColumns) {
+               $this->referencedColumns = $referencedColumns;
+               
+               return $this;
+       }
+       
+       /**
+        * Sets the name of the referenced table and returns the foreign key.
+        *
+        * @param       string          $referencedTable        name of referenced table
+        * @return      $this                                   this foreign key
+        */
+       public function referencedTable($referencedTable) {
+               $this->referencedTable = ApplicationHandler::insertRealDatabaseTableNames($referencedTable);
+               
+               return $this;
+       }
+       
+       /**
+        * Returns a `DatabaseTableForeignKey` object with the given name.
+        * 
+        * @param       string          $name
+        * @return      static
+        */
+       public static function create($name = '') {
+               return new static($name);
+       }
+       
+       /**
+        * Returns a `DatabaseTableForeignKey` object with the given name and data.
+        * 
+        * @param       string          $name
+        * @param       array           $data           data returned by `DatabaseEditor::getForeignKeys()`
+        * @return      static
+        */
+       public static function createFromData($name, $data) {
+               return static::create($name)
+                               ->columns($data['columns'])
+                               ->onDelete($data['ON DELETE'])
+                               ->onUpdate($data['ON UPDATE'])
+                               ->referencedColumns($data['referencedColumns'])
+                               ->referencedTable($data['referencedTable']);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/index/DatabaseTableIndex.class.php b/wcfsetup/install/files/lib/system/database/table/index/DatabaseTableIndex.class.php
new file mode 100644 (file)
index 0000000..747001e
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+namespace wcf\system\database\table\index;
+use wcf\system\database\table\TDroppableDatabaseComponent;
+
+/**
+ * Represents an index of a database table.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Index
+ * @since      5.2
+ */
+class DatabaseTableIndex {
+       use TDroppableDatabaseComponent;
+       
+       /**
+        * indexed columns
+        * @var string[]
+        */
+       protected $columns;
+       
+       /**
+        * name of index
+        * @var string
+        */
+       protected $name;
+       
+       /**
+        * type of index (see `*_TYPE` constants)
+        * @var null|string
+        */
+       protected $type;
+       
+       const DEFAULT_TYPE = null;
+       const PRIMARY_TYPE = 'PRIMARY';
+       const UNIQUE_TYPE = 'UNIQUE';
+       const FULLTEXT_TYPE = 'FULLTEXT';
+       
+       /**
+        * Creates a new `DatabaseTableIndex` object.
+        * 
+        * @param       string          $name           column name
+        */
+       protected function __construct($name) {
+               $this->name = $name;
+       }
+       
+       /**
+        * Sets the indexed columns and returns the index.
+        * 
+        * @param       string[]        $columns        indexed columns
+        * @return      $this                           this index
+        */
+       public function columns($columns) {
+               $this->columns = array_values($columns);
+               
+               return $this;
+       }
+       
+       /**
+        * Returns the index columns.
+        * 
+        * @return      string[]
+        */
+       public function getColumns() {
+               return $this->columns;
+       }
+       
+       /**
+        * Returns the data used by `DatabaseEditor` to add the index to a table.
+        * 
+        * @return      array
+        */
+       public function getData() {
+               return [
+                       'columns' => implode(',', $this->columns),
+                       'type' => $this->type
+               ];
+       }
+       
+       /**
+        * Returns the name of the index.
+        * 
+        * @return      string
+        */
+       public function getName() {
+               return $this->name;
+       }
+       
+       /**
+        * Returns the type of the index (see `*_TYPE` constants).
+        * 
+        * @return      null|string
+        */
+       public function getType() {
+               return $this->type;
+       }
+       
+       /**
+        * Sets the name of the index.
+        * 
+        * @param       string          $name           index name
+        * @return      $this                           this index
+        */
+       public function name($name) {
+               $this->name = $name;
+               
+               return $this;
+       }
+       
+       /**
+        * Sets the type of the index and returns the index
+        * 
+        * @param       null|string     $type           index type
+        * @return      $this                           this index
+        * @throws      \InvalidArgumentException       if given type is invalid
+        */
+       public function type($type) {
+               if ($type !== static::DEFAULT_TYPE && $type !== static::PRIMARY_TYPE && $type !== static::UNIQUE_TYPE && $type !== static::FULLTEXT_TYPE) {
+                       throw new \InvalidArgumentException("Unknown index type '{$type}'.");
+               }
+               
+               $this->type = $type;
+               
+               return $this;
+       }
+       
+       /**
+        * Returns a `DatabaseTableIndex` object with the given name.
+        * 
+        * @param       string          $name
+        * @return      static
+        */
+       public static function create($name = '') {
+               return new static($name);
+       }
+       
+       /**
+        * Returns a `DatabaseTableIndex` object with the given name and data.
+        * 
+        * @param       string          $name
+        * @param       array           $data           data returned by `DatabaseEditor::getIndexInformation()`
+        * @return      static
+        */
+       public static function createFromData($name, array $data) {
+               return static::create($name)
+                       ->type($data['type'])
+                       ->columns($data['columns']);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/database/table/index/DatabaseTablePrimaryIndex.class.php b/wcfsetup/install/files/lib/system/database/table/index/DatabaseTablePrimaryIndex.class.php
new file mode 100644 (file)
index 0000000..964c6fb
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+namespace wcf\system\database\table\index;
+
+/**
+ * Represents a primary index of a database table.
+ * 
+ * This class just provides a shorter factory method that automatically sets the name and type of
+ * the primary index.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Database\Table\Index
+ * @since      5.2
+ */
+class DatabaseTablePrimaryIndex extends DatabaseTableIndex {
+       /**
+        * Returns a `PrimaryDatabaseTableIndex` object with `PRIMARY` as name and primary as type.
+        * 
+        * @return      $this
+        */
+       public static function create() {
+               return parent::create('PRIMARY')
+                       ->type(static::PRIMARY_TYPE);
+       }
+}
index d280066aad2f216a93e1f0c689d89e6f5dd25c8a..61f952f3eb34a41e5e9fe9e702e814873ba42eb5 100644 (file)
@@ -1937,6 +1937,26 @@ Die Datenbestände werden sorgfältig gepflegt, aber es ist nicht ausgeschlossen
                <item name="wcf.acp.package.search.status.refreshDatabase"><![CDATA[Die Paketlisten werden aktualisiert …]]></item>
                <item name="wcf.acp.package.search.result.thirdParty"><![CDATA[{if $count === 0}Keine{elseif $count === 1}Ein{else}{#$count}{/if} Treffer in Paketquellen von Drittanbietern]]></item>
                <item name="wcf.acp.package.search.result.trusted"><![CDATA[{if $count === 0}Keine{elseif $count === 1}Ein{else}{#$count}{/if} Treffer in offizielle Paketquellen]]></item>
+               <item name="wcf.acp.package.error.databaseChange"><![CDATA[Das Datenbanklayout konnte aufgrund folgender Fehler nicht aktualisiert werden:\r
+{implode from=$errors item=error glue=' '}\r
+       {if $error[type] === 'unregisteredTableDrop'}\r
+               Die unbekannte Tabelle {$error[tableName]} kann nicht gelöscht werden.\r
+       {else if $error[type] === 'foreignTableDrop'}\r
+               Die Tabelle {$error[tableName]} gehört zu einem anderen Paket und kann deshalb nicht gelöscht werden.\r
+       {else if $error[type] === 'unregisteredTableChange'}\r
+               Die unbekannte Tabelle {$error[tableName]} kann nicht verändert werden.\r
+       {else if $error[type] === 'foreignColumnDrop'}\r
+               Die Spalte {$error[tableName]}.{$error[columnName]} gehört zu einem anderen Paket und kann deshalb nicht gelöscht werden.\r
+       {else if $error[type] === 'foreignColumnChange'}\r
+               Die Spalte {$error[tableName]}.{$error[columnName]} gehört zu einem anderen Paket und kann deshalb nicht gelöscht werden.\r
+       {else if $error[type] === 'foreignIndexDrop'}\r
+               Der Index {$error[tableName]} ({$error[columnNames]}) gehört zu einem anderen Paket und kann deshalb nicht gelöscht werden.\r
+       {else if $error[type] === 'foreignForeignKeyDrop'}\r
+               Der Fremdschlüssel {$error[tableName]} ({$error[columnNames]}) gehört zu einem anderen Paket und kann deshalb nicht gelöscht werden.\r
+       {else}\r
+               Unbekannter Fehler.\r
+       {/if}\r
+{/implode}]]></item>
        </category>
        <category name="wcf.acp.page">
                <item name="wcf.acp.page.add"><![CDATA[Seite hinzufügen]]></item>
index 2dac401c24d312464921ab3d56b2e2233b2dc163..6ffc87b6043218478b19fac3e1ab987585dffb95 100644 (file)
@@ -1922,6 +1922,26 @@ If you have <strong>already bought the licenses for the listed apps</strong>, th
                <item name="wcf.acp.package.search.status.refreshDatabase"><![CDATA[Retrieving the package lists…]]></item>
                <item name="wcf.acp.package.search.result.thirdParty"><![CDATA[{if $count === 0}No matches{elseif $count === 1}One match{else}{#$count} matches{/if} in package sources maintained by third parties]]></item>
                <item name="wcf.acp.package.search.result.trusted"><![CDATA[{if $count === 0}No matches{elseif $count === 1}One match{else}{#$count} matches{/if} in official package sources]]></item>
+               <item name="wcf.acp.package.error.databaseChange"><![CDATA[The database layout could not be changed because of the following errors:\r
+{implode from=$errors item=error glue=' '}\r
+       {if $error[type] === 'unregisteredTableDrop'}\r
+               The unknown table {$error[tableName]} cannot be dropped.\r
+       {else if $error[type] === 'foreignTableDrop'}\r
+               The table {$error[tableName]} belongs to a different package and thus cannot be dropped.\r
+       {else if $error[type] === 'unregisteredTableChange'}\r
+               The unknown table {$error[tableName]} cannot be altered.\r
+       {else if $error[type] === 'foreignColumnDrop'}\r
+               The column {$error[tableName]}.{$error[columnName]} belongs to a different package and thus cannot be dropped.\r
+       {else if $error[type] === 'foreignColumnChange'}\r
+               The column {$error[tableName]}.{$error[columnName]} belongs to a different package and thus cannot be dropped.\r
+       {else if $error[type] === 'foreignIndexDrop'}\r
+               The index {$error[tableName]} ({$error[columnNames]}) belongs to a different package and thus cannot be dropped.\r
+       {else if $error[type] === 'foreignForeignKeyDrop'}\r
+               The foreign key {$error[tableName]} ({$error[columnNames]}) belongs to a different package and thus cannot be dropped.\r
+       {else}\r
+               Unknown error.\r
+       {/if}\r
+{/implode}]]></item>
        </category>
        <category name="wcf.acp.paidSubscription">
                <item name="wcf.acp.paidSubscription.list"><![CDATA[Paid Subscriptions]]></item>