Commit | Line | Data |
---|---|---|
f6e43f2f MS |
1 | <?php |
2 | namespace wcf\system\database\table; | |
3 | use wcf\data\package\Package; | |
4 | use wcf\system\database\editor\DatabaseEditor; | |
77d58141 | 5 | use wcf\system\database\table\column\AbstractIntDatabaseTableColumn; |
f6e43f2f | 6 | use wcf\system\database\table\column\IDatabaseTableColumn; |
77d58141 | 7 | use wcf\system\database\table\column\TinyintDatabaseTableColumn; |
f6e43f2f MS |
8 | use wcf\system\database\table\index\DatabaseTableForeignKey; |
9 | use wcf\system\database\table\index\DatabaseTableIndex; | |
10 | use wcf\system\database\util\PreparedStatementConditionBuilder; | |
11 | use wcf\system\package\SplitNodeException; | |
12 | use wcf\system\WCF; | |
13 | ||
14 | /** | |
15 | * Processes a given set of changes to database tables. | |
16 | * | |
17 | * @author Matthias Schmidt | |
77d58141 | 18 | * @copyright 2001-2020 WoltLab GmbH |
f6e43f2f MS |
19 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> |
20 | * @package WoltLabSuite\Core\System\Database\Table | |
21 | * @since 5.2 | |
22 | */ | |
23 | class DatabaseTableChangeProcessor { | |
24 | /** | |
3de2e191 MS |
25 | * maps the registered database table column names to the ids of the packages they belong to |
26 | * @var int[][] | |
f6e43f2f | 27 | */ |
3de2e191 | 28 | protected $columnPackageIDs = []; |
f6e43f2f MS |
29 | |
30 | /** | |
3de2e191 MS |
31 | * database table columns that will be added grouped by the name of the table to which they |
32 | * will be added | |
33 | * @var IDatabaseTableColumn[][] | |
f6e43f2f | 34 | */ |
3de2e191 | 35 | protected $columnsToAdd = []; |
f6e43f2f MS |
36 | |
37 | /** | |
3de2e191 MS |
38 | * database table columns that will be altered grouped by the name of the table to which |
39 | * they belong | |
40 | * @var IDatabaseTableColumn[][] | |
f6e43f2f | 41 | */ |
3de2e191 | 42 | protected $columnsToAlter = []; |
f6e43f2f MS |
43 | |
44 | /** | |
3de2e191 MS |
45 | * database table columns that will be dropped grouped by the name of the table from which |
46 | * they will be dropped | |
47 | * @var IDatabaseTableColumn[][] | |
f6e43f2f | 48 | */ |
3de2e191 | 49 | protected $columnsToDrop = []; |
f6e43f2f MS |
50 | |
51 | /** | |
52 | * database editor to apply the relevant changes to the table layouts | |
53 | * @var DatabaseEditor | |
54 | */ | |
55 | protected $dbEditor; | |
3de2e191 | 56 | |
f6e43f2f | 57 | /** |
3de2e191 MS |
58 | * list of all existing tables in the used database |
59 | * @var string[] | |
f6e43f2f | 60 | */ |
3de2e191 | 61 | protected $existingTableNames = []; |
f6e43f2f MS |
62 | |
63 | /** | |
3de2e191 MS |
64 | * existing database tables |
65 | * @var DatabaseTable[] | |
f6e43f2f | 66 | */ |
3de2e191 | 67 | protected $existingTables = []; |
f6e43f2f MS |
68 | |
69 | /** | |
3de2e191 MS |
70 | * maps the registered database table index names to the ids of the packages they belong to |
71 | * @var int[][] | |
f6e43f2f | 72 | */ |
3de2e191 | 73 | protected $indexPackageIDs = []; |
f6e43f2f MS |
74 | |
75 | /** | |
3de2e191 MS |
76 | * indices that will be added grouped by the name of the table to which they will be added |
77 | * @var DatabaseTableIndex[][] | |
f6e43f2f | 78 | */ |
3de2e191 | 79 | protected $indicesToAdd = []; |
f6e43f2f MS |
80 | |
81 | /** | |
3de2e191 MS |
82 | * indices that will be dropped grouped by the name of the table from which they will be dropped |
83 | * @var DatabaseTableIndex[][] | |
f6e43f2f | 84 | */ |
3de2e191 | 85 | protected $indicesToDrop = []; |
f6e43f2f MS |
86 | |
87 | /** | |
88 | * maps the registered database table foreign key names to the ids of the packages they belong to | |
89 | * @var int[][] | |
90 | */ | |
91 | protected $foreignKeyPackageIDs = []; | |
92 | ||
3de2e191 MS |
93 | /** |
94 | * foreign keys that will be added grouped by the name of the table to which they will be | |
95 | * added | |
96 | * @var DatabaseTableForeignKey[][] | |
97 | */ | |
98 | protected $foreignKeysToAdd = []; | |
99 | ||
100 | /** | |
101 | * foreign keys that will be dropped grouped by the name of the table from which they will | |
102 | * be dropped | |
103 | * @var DatabaseTableForeignKey[][] | |
104 | */ | |
105 | protected $foreignKeysToDrop = []; | |
106 | ||
f6e43f2f MS |
107 | /** |
108 | * package that wants to apply the changes | |
109 | * @var Package | |
110 | */ | |
111 | protected $package; | |
112 | ||
3de2e191 MS |
113 | /** |
114 | * message for the split node exception thrown after the changes have been applied | |
115 | * @var string | |
116 | */ | |
117 | protected $splitNodeMessage = ''; | |
118 | ||
f6e43f2f MS |
119 | /** |
120 | * layouts/layout changes of the relevant database table | |
121 | * @var DatabaseTable[] | |
122 | */ | |
123 | protected $tables; | |
124 | ||
125 | /** | |
126 | * maps the registered database table names to the ids of the packages they belong to | |
127 | * @var int[] | |
128 | */ | |
129 | protected $tablePackageIDs = []; | |
130 | ||
3de2e191 MS |
131 | /** |
132 | * database table that will be created | |
133 | * @var DatabaseTable[] | |
134 | */ | |
135 | protected $tablesToCreate = []; | |
136 | ||
137 | /** | |
138 | * database tables that will be dropped | |
139 | * @var DatabaseTable[] | |
140 | */ | |
141 | protected $tablesToDrop = []; | |
142 | ||
f6e43f2f MS |
143 | /** |
144 | * Creates a new instance of `DatabaseTableChangeProcessor`. | |
145 | * | |
146 | * @param Package $package | |
147 | * @param DatabaseTable[] $tables | |
148 | * @param DatabaseEditor $dbEditor | |
f6e43f2f | 149 | */ |
d9441130 | 150 | public function __construct(Package $package, array $tables, DatabaseEditor $dbEditor) { |
f6e43f2f MS |
151 | $this->package = $package; |
152 | ||
153 | $tableNames = []; | |
154 | foreach ($tables as $table) { | |
155 | if (!($table instanceof DatabaseTable)) { | |
156 | throw new \InvalidArgumentException("Tables must be instance of '" . DatabaseTable::class . "'"); | |
157 | } | |
158 | ||
159 | $tableNames[] = $table->getName(); | |
160 | } | |
161 | ||
162 | $this->tables = $tables; | |
163 | $this->dbEditor = $dbEditor; | |
f6e43f2f MS |
164 | |
165 | $this->existingTableNames = $dbEditor->getTableNames(); | |
166 | ||
167 | $conditionBuilder = new PreparedStatementConditionBuilder(); | |
168 | $conditionBuilder->add('sqlTable IN (?)', [$tableNames]); | |
3de2e191 | 169 | $conditionBuilder->add('isDone = ?', [1]); |
f6e43f2f MS |
170 | |
171 | $sql = "SELECT * | |
3de2e191 | 172 | FROM wcf" . WCF_N . "_package_installation_sql_log |
f6e43f2f MS |
173 | " . $conditionBuilder; |
174 | $statement = WCF::getDB()->prepareStatement($sql); | |
175 | $statement->execute($conditionBuilder->getParameters()); | |
176 | ||
177 | while ($row = $statement->fetchArray()) { | |
178 | if ($row['sqlIndex'] === '' && $row['sqlColumn'] === '') { | |
179 | $this->tablePackageIDs[$row['sqlTable']] = $row['packageID']; | |
180 | } | |
181 | else if ($row['sqlIndex'] === '') { | |
182 | $this->columnPackageIDs[$row['sqlTable']][$row['sqlColumn']] = $row['packageID']; | |
183 | } | |
184 | else if (substr($row['sqlIndex'], -3) === '_fk') { | |
185 | $this->foreignKeyPackageIDs[$row['sqlTable']][$row['sqlIndex']] = $row['packageID']; | |
186 | } | |
187 | else { | |
188 | $this->indexPackageIDs[$row['sqlTable']][$row['sqlIndex']] = $row['packageID']; | |
189 | } | |
190 | } | |
191 | } | |
192 | ||
193 | /** | |
3de2e191 | 194 | * Adds the given index to the table. |
f6e43f2f | 195 | * |
3de2e191 MS |
196 | * @param string $tableName |
197 | * @param DatabaseTableForeignKey $foreignKey | |
f6e43f2f | 198 | */ |
3de2e191 MS |
199 | protected function addForeignKey($tableName, DatabaseTableForeignKey $foreignKey) { |
200 | $this->dbEditor->addForeignKey($tableName, $foreignKey->getName(), $foreignKey->getData()); | |
f6e43f2f MS |
201 | } |
202 | ||
203 | /** | |
3de2e191 MS |
204 | * Adds the given index to the table. |
205 | * | |
206 | * @param string $tableName | |
207 | * @param DatabaseTableIndex $index | |
f6e43f2f | 208 | */ |
3de2e191 MS |
209 | protected function addIndex($tableName, DatabaseTableIndex $index) { |
210 | $this->dbEditor->addIndex($tableName, $index->getName(), $index->getData()); | |
f6e43f2f MS |
211 | } |
212 | ||
213 | /** | |
3de2e191 | 214 | * Applies all of the previously determined changes to achieve the desired database layout. |
f6e43f2f | 215 | * |
3de2e191 | 216 | * @throws SplitNodeException if any change has been applied |
f6e43f2f | 217 | */ |
3de2e191 MS |
218 | protected function applyChanges() { |
219 | $appliedAnyChange = false; | |
f6e43f2f | 220 | |
3de2e191 MS |
221 | foreach ($this->tablesToCreate as $table) { |
222 | $appliedAnyChange = true; | |
223 | ||
224 | $this->prepareTableLog($table); | |
225 | $this->createTable($table); | |
226 | $this->finalizeTableLog($table); | |
f6e43f2f MS |
227 | } |
228 | ||
3de2e191 MS |
229 | foreach ($this->tablesToDrop as $table) { |
230 | $appliedAnyChange = true; | |
f6e43f2f | 231 | |
3de2e191 MS |
232 | $this->dropTable($table); |
233 | $this->deleteTableLog($table); | |
f6e43f2f MS |
234 | } |
235 | ||
3de2e191 MS |
236 | $columnTables = array_unique(array_merge( |
237 | array_keys($this->columnsToAdd), | |
238 | array_keys($this->columnsToAlter), | |
239 | array_keys($this->columnsToDrop) | |
240 | )); | |
241 | foreach ($columnTables as $tableName) { | |
242 | $appliedAnyChange = true; | |
f6e43f2f | 243 | |
3de2e191 MS |
244 | $columnsToAdd = $this->columnsToAdd[$tableName] ?? []; |
245 | $columnsToAlter = $this->columnsToAlter[$tableName] ?? []; | |
246 | $columnsToDrop = $this->columnsToDrop[$tableName] ?? []; | |
247 | ||
248 | foreach ($columnsToAdd as $column) { | |
249 | $this->prepareColumnLog($tableName, $column); | |
f6e43f2f | 250 | } |
f6e43f2f | 251 | |
3de2e191 MS |
252 | $this->applyColumnChanges( |
253 | $tableName, | |
254 | $columnsToAdd, | |
255 | $columnsToAlter, | |
256 | $columnsToDrop | |
257 | ); | |
258 | ||
259 | foreach ($columnsToAdd as $column) { | |
260 | $this->finalizeColumnLog($tableName, $column); | |
261 | } | |
262 | ||
263 | foreach ($columnsToDrop as $column) { | |
264 | $this->deleteColumnLog($tableName, $column); | |
f6e43f2f | 265 | } |
f6e43f2f MS |
266 | } |
267 | ||
c64a33b4 | 268 | foreach ($this->foreignKeysToDrop as $tableName => $foreignKeys) { |
3de2e191 MS |
269 | foreach ($foreignKeys as $foreignKey) { |
270 | $appliedAnyChange = true; | |
271 | ||
c64a33b4 MS |
272 | $this->dropForeignKey($tableName, $foreignKey); |
273 | $this->deleteForeignKeyLog($tableName, $foreignKey); | |
3de2e191 | 274 | } |
f6e43f2f MS |
275 | } |
276 | ||
c64a33b4 | 277 | foreach ($this->foreignKeysToAdd as $tableName => $foreignKeys) { |
3de2e191 MS |
278 | foreach ($foreignKeys as $foreignKey) { |
279 | $appliedAnyChange = true; | |
280 | ||
c64a33b4 MS |
281 | $this->prepareForeignKeyLog($tableName, $foreignKey); |
282 | $this->addForeignKey($tableName, $foreignKey); | |
283 | $this->finalizeForeignKeyLog($tableName, $foreignKey); | |
f6e43f2f MS |
284 | } |
285 | } | |
286 | ||
c8280fb8 | 287 | foreach ($this->indicesToDrop as $tableName => $indices) { |
f6e43f2f | 288 | foreach ($indices as $index) { |
3de2e191 MS |
289 | $appliedAnyChange = true; |
290 | ||
c8280fb8 MS |
291 | $this->dropIndex($tableName, $index); |
292 | $this->deleteIndexLog($tableName, $index); | |
f6e43f2f MS |
293 | } |
294 | } | |
295 | ||
c8280fb8 | 296 | foreach ($this->indicesToAdd as $tableName => $indices) { |
3de2e191 MS |
297 | foreach ($indices as $index) { |
298 | $appliedAnyChange = true; | |
299 | ||
c8280fb8 MS |
300 | $this->prepareIndexLog($tableName, $index); |
301 | $this->addIndex($tableName, $index); | |
302 | $this->finalizeIndexLog($tableName, $index); | |
f6e43f2f | 303 | } |
3de2e191 MS |
304 | } |
305 | ||
306 | if ($appliedAnyChange) { | |
307 | throw new SplitNodeException($this->splitNodeMessage); | |
f6e43f2f MS |
308 | } |
309 | } | |
310 | ||
311 | /** | |
3de2e191 | 312 | * Adds, alters, and drop columns of the same table. |
f6e43f2f | 313 | * |
3de2e191 MS |
314 | * Before a column is dropped, all of its foreign keys are dropped. |
315 | * | |
316 | * @param string $tableName | |
317 | * @param IDatabaseTableColumn[] $addedColumns | |
318 | * @param IDatabaseTableColumn[] $alteredColumns | |
319 | * @param IDatabaseTableColumn[] $droppedColumns | |
f6e43f2f | 320 | */ |
3de2e191 MS |
321 | protected function applyColumnChanges($tableName, array $addedColumns, array $alteredColumns, array $droppedColumns) { |
322 | $dropForeignKeys = []; | |
323 | ||
324 | $columnData = []; | |
325 | foreach ($droppedColumns as $droppedColumn) { | |
326 | $columnData[$droppedColumn->getName()] = [ | |
327 | 'action' => 'drop' | |
328 | ]; | |
329 | ||
330 | foreach ($this->getExistingTable($tableName)->getForeignKeys() as $foreignKey) { | |
331 | if (in_array($droppedColumn->getName(), $foreignKey->getColumns())) { | |
4b07e8c2 | 332 | $dropForeignKeys[] = $foreignKey; |
3de2e191 MS |
333 | } |
334 | } | |
335 | } | |
336 | foreach ($addedColumns as $addedColumn) { | |
337 | $columnData[$addedColumn->getName()] = [ | |
338 | 'action' => 'add', | |
339 | 'data' => $addedColumn->getData() | |
340 | ]; | |
341 | } | |
342 | foreach ($alteredColumns as $alteredColumn) { | |
343 | $columnData[$alteredColumn->getName()] = [ | |
344 | 'action' => 'alter', | |
345 | 'data' => $alteredColumn->getData(), | |
346 | 'oldColumnName' => $alteredColumn->getName() | |
347 | ]; | |
f6e43f2f MS |
348 | } |
349 | ||
3de2e191 MS |
350 | if (!empty($columnData)) { |
351 | foreach ($dropForeignKeys as $foreignKey) { | |
4b07e8c2 MS |
352 | $this->dropForeignKey($tableName, $foreignKey); |
353 | $this->deleteForeignKeyLog($tableName, $foreignKey); | |
3de2e191 MS |
354 | } |
355 | ||
356 | $this->dbEditor->alterColumns($tableName, $columnData); | |
357 | } | |
358 | } | |
359 | ||
360 | /** | |
361 | * Calculates all of the necessary changes to be executed. | |
362 | */ | |
363 | protected function calculateChanges() { | |
f6e43f2f | 364 | foreach ($this->tables as $table) { |
3de2e191 MS |
365 | $tableName = $table->getName(); |
366 | ||
f6e43f2f | 367 | if ($table->willBeDropped()) { |
3de2e191 MS |
368 | if (in_array($tableName, $this->existingTableNames)) { |
369 | $this->tablesToDrop[] = $table; | |
370 | ||
d9441130 MS |
371 | $this->splitNodeMessage .= "Dropped table '{$tableName}'."; |
372 | break; | |
3de2e191 MS |
373 | } |
374 | else if (isset($this->tablePackageIDs[$tableName])) { | |
375 | $this->deleteTableLog($table); | |
f6e43f2f MS |
376 | } |
377 | } | |
3de2e191 | 378 | else if (!in_array($tableName, $this->existingTableNames)) { |
e00d344b MS |
379 | if ($table instanceof PartialDatabaseTable) { |
380 | throw new \LogicException("Partial table '{$tableName}' cannot be created."); | |
381 | } | |
382 | ||
3de2e191 MS |
383 | $this->tablesToCreate[] = $table; |
384 | ||
d9441130 MS |
385 | $this->splitNodeMessage .= "Created table '{$tableName}'."; |
386 | break; | |
f6e43f2f MS |
387 | } |
388 | else { | |
389 | // calculate difference between tables | |
3de2e191 | 390 | $existingTable = $this->getExistingTable($tableName); |
f6e43f2f | 391 | $existingColumns = $existingTable->getColumns(); |
f6e43f2f | 392 | |
f6e43f2f | 393 | foreach ($table->getColumns() as $column) { |
3de2e191 MS |
394 | if ($column->willBeDropped()) { |
395 | if (isset($existingColumns[$column->getName()])) { | |
396 | if (!isset($this->columnsToDrop[$tableName])) { | |
397 | $this->columnsToDrop[$tableName] = []; | |
398 | } | |
399 | $this->columnsToDrop[$tableName][] = $column; | |
400 | } | |
401 | else if (isset($this->columnPackageIDs[$tableName][$column->getName()])) { | |
402 | $this->deleteColumnLog($tableName, $column); | |
403 | } | |
f6e43f2f | 404 | } |
3de2e191 MS |
405 | else if (!isset($existingColumns[$column->getName()])) { |
406 | if (!isset($this->columnsToAdd[$tableName])) { | |
407 | $this->columnsToAdd[$tableName] = []; | |
f6e43f2f | 408 | } |
3de2e191 MS |
409 | $this->columnsToAdd[$tableName][] = $column; |
410 | } | |
689cb90b | 411 | else if ($this->diffColumns($existingColumns[$column->getName()], $column)) { |
3de2e191 MS |
412 | if (!isset($this->columnsToAlter[$tableName])) { |
413 | $this->columnsToAlter[$tableName] = []; | |
f6e43f2f | 414 | } |
3de2e191 | 415 | $this->columnsToAlter[$tableName][] = $column; |
f6e43f2f MS |
416 | } |
417 | } | |
418 | ||
3de2e191 MS |
419 | // all column-related changes are executed in one query thus break |
420 | // here and not within the previous loop | |
d9441130 | 421 | if (!empty($this->columnsToAdd) || !empty($this->columnsToAlter) || !empty($this->columnsToDrop)) { |
3de2e191 MS |
422 | $this->splitNodeMessage .= "Altered columns of table '{$tableName}'."; |
423 | break; | |
424 | } | |
f6e43f2f | 425 | |
3de2e191 | 426 | $existingForeignKeys = $existingTable->getForeignKeys(); |
f6e43f2f MS |
427 | foreach ($table->getForeignKeys() as $foreignKey) { |
428 | $matchingExistingForeignKey = null; | |
429 | foreach ($existingForeignKeys as $existingForeignKey) { | |
c64a33b4 | 430 | if (empty(array_diff($foreignKey->getDiffData(), $existingForeignKey->getDiffData()))) { |
f6e43f2f MS |
431 | $matchingExistingForeignKey = $existingForeignKey; |
432 | break; | |
433 | } | |
434 | } | |
435 | ||
436 | if ($foreignKey->willBeDropped()) { | |
437 | if ($matchingExistingForeignKey !== null) { | |
3de2e191 MS |
438 | if (!isset($this->foreignKeysToDrop[$tableName])) { |
439 | $this->foreignKeysToDrop[$tableName] = []; | |
440 | } | |
441 | $this->foreignKeysToDrop[$tableName][] = $foreignKey; | |
442 | ||
d9441130 MS |
443 | $this->splitNodeMessage .= "Dropped foreign key '{$tableName}." . implode(',', $foreignKey->getColumns()) . "'."; |
444 | break 2; | |
3de2e191 MS |
445 | } |
446 | else if (isset($this->foreignKeyPackageIDs[$tableName][$foreignKey->getName()])) { | |
447 | $this->deleteForeignKeyLog($tableName, $foreignKey); | |
f6e43f2f MS |
448 | } |
449 | } | |
450 | else if ($matchingExistingForeignKey === null) { | |
dc4b5734 MS |
451 | // If the referenced database table does not already exists, delay the |
452 | // foreign key creation until after the referenced table has been created. | |
453 | if (!in_array($foreignKey->getReferencedTable(), $this->existingTableNames)) { | |
454 | continue; | |
455 | } | |
456 | ||
3de2e191 MS |
457 | if (!isset($this->foreignKeysToAdd[$tableName])) { |
458 | $this->foreignKeysToAdd[$tableName] = []; | |
459 | } | |
460 | $this->foreignKeysToAdd[$tableName][] = $foreignKey; | |
461 | ||
d9441130 MS |
462 | $this->splitNodeMessage .= "Added foreign key '{$tableName}." . implode(',', $foreignKey->getColumns()) . "'."; |
463 | break 2; | |
f6e43f2f | 464 | } |
5963110a | 465 | else if (!empty(array_diff($foreignKey->getData(), $matchingExistingForeignKey->getData()))) { |
c64a33b4 MS |
466 | if (!isset($this->foreignKeysToDrop[$tableName])) { |
467 | $this->foreignKeysToDrop[$tableName] = []; | |
468 | } | |
469 | $this->foreignKeysToDrop[$tableName][] = $matchingExistingForeignKey; | |
470 | ||
471 | if (!isset($this->foreignKeysToAdd[$tableName])) { | |
472 | $this->foreignKeysToAdd[$tableName] = []; | |
473 | } | |
474 | $this->foreignKeysToAdd[$tableName][] = $foreignKey; | |
475 | ||
476 | $this->splitNodeMessage .= "Replaced foreign key '{$tableName}." . implode(',', $foreignKey->getColumns()) . "'."; | |
477 | break 2; | |
478 | } | |
f6e43f2f MS |
479 | } |
480 | ||
3de2e191 | 481 | $existingIndices = $existingTable->getIndices(); |
f6e43f2f MS |
482 | foreach ($table->getIndices() as $index) { |
483 | $matchingExistingIndex = null; | |
484 | foreach ($existingIndices as $existingIndex) { | |
c8280fb8 | 485 | if (!$this->diffIndices($existingIndex, $index)) { |
f6e43f2f MS |
486 | $matchingExistingIndex = $existingIndex; |
487 | break; | |
488 | } | |
489 | } | |
490 | ||
491 | if ($index->willBeDropped()) { | |
492 | if ($matchingExistingIndex !== null) { | |
3de2e191 MS |
493 | if (!isset($this->indicesToDrop[$tableName])) { |
494 | $this->indicesToDrop[$tableName] = []; | |
495 | } | |
a9e05ee0 | 496 | $this->indicesToDrop[$tableName][] = $matchingExistingIndex; |
3de2e191 | 497 | |
d9441130 MS |
498 | $this->splitNodeMessage .= "Dropped index '{$tableName}." . implode(',', $index->getColumns()) . "'."; |
499 | break 2; | |
3de2e191 MS |
500 | } |
501 | else if (isset($this->indexPackageIDs[$tableName][$index->getName()])) { | |
502 | $this->deleteIndexLog($tableName, $index); | |
f6e43f2f MS |
503 | } |
504 | } | |
c8280fb8 MS |
505 | else if ($matchingExistingIndex !== null) { |
506 | // updating index type and index columns is supported with an | |
507 | // explicit index name is given (automatically generated index | |
508 | // names are not deterministic) | |
509 | if (!$index->hasGeneratedName() && !empty(array_diff($matchingExistingIndex->getData(), $index->getData()))) { | |
510 | if (!isset($this->indicesToDrop[$tableName])) { | |
511 | $this->indicesToDrop[$tableName] = []; | |
512 | } | |
513 | $this->indicesToDrop[$tableName][] = $matchingExistingIndex; | |
514 | ||
515 | if (!isset($this->indicesToAdd[$tableName])) { | |
516 | $this->indicesToAdd[$tableName] = []; | |
517 | } | |
518 | $this->indicesToAdd[$tableName][] = $index; | |
519 | } | |
520 | } | |
521 | else { | |
3de2e191 MS |
522 | if (!isset($this->indicesToAdd[$tableName])) { |
523 | $this->indicesToAdd[$tableName] = []; | |
524 | } | |
525 | $this->indicesToAdd[$tableName][] = $index; | |
526 | ||
d9441130 MS |
527 | $this->splitNodeMessage .= "Added index '{$tableName}." . implode(',', $index->getColumns()) . "'."; |
528 | break 2; | |
f6e43f2f MS |
529 | } |
530 | } | |
f6e43f2f MS |
531 | } |
532 | } | |
3de2e191 MS |
533 | } |
534 | ||
535 | /** | |
536 | * Checks for any pending log entries for the package and either marks them as done or | |
537 | * deletes them so that after this method finishes, there are no more undone log entries | |
538 | * for the package. | |
539 | */ | |
540 | protected function checkPendingLogEntries() { | |
541 | $sql = "SELECT * | |
542 | FROM wcf" . WCF_N . "_package_installation_sql_log | |
543 | WHERE packageID = ? | |
544 | AND isDone = ?"; | |
545 | $statement = WCF::getDB()->prepareStatement($sql); | |
546 | $statement->execute([$this->package->packageID, 0]); | |
547 | ||
548 | $doneEntries = $undoneEntries = []; | |
549 | while ($row = $statement->fetchArray()) { | |
550 | // table | |
551 | if ($row['sqlIndex'] === '' && $row['sqlColumn'] === '') { | |
552 | if (in_array($row['sqlTable'], $this->existingTableNames)) { | |
553 | $doneEntries[] = $row; | |
554 | } | |
555 | else { | |
556 | $undoneEntries[] = $row; | |
557 | } | |
558 | } | |
559 | // column | |
560 | else if ($row['sqlIndex'] === '') { | |
561 | if (isset($this->getExistingTable($row['sqlTable'])->getColumns()[$row['sqlColumn']])) { | |
562 | $doneEntries[] = $row; | |
563 | } | |
564 | else { | |
565 | $undoneEntries[] = $row; | |
566 | } | |
567 | } | |
568 | // foreign key | |
569 | else if (substr($row['sqlIndex'], -3) === '_fk') { | |
570 | if (isset($this->getExistingTable($row['sqlTable'])->getForeignKeys()[$row['sqlIndex']])) { | |
571 | $doneEntries[] = $row; | |
572 | } | |
573 | else { | |
574 | $undoneEntries[] = $row; | |
575 | } | |
576 | } | |
577 | // index | |
578 | else { | |
579 | if (isset($this->getExistingTable($row['sqlTable'])->getIndices()[$row['sqlIndex']])) { | |
580 | $doneEntries[] = $row; | |
581 | } | |
582 | else { | |
583 | $undoneEntries[] = $row; | |
584 | } | |
585 | } | |
586 | } | |
587 | ||
588 | WCF::getDB()->beginTransaction(); | |
589 | foreach ($doneEntries as $entry) { | |
590 | $this->finalizeLog($entry); | |
591 | } | |
f6e43f2f | 592 | |
3de2e191 MS |
593 | // to achieve a consistent state, undone log entries will be deleted here even though |
594 | // they might be re-created later to ensure that after this method finishes, there are | |
595 | // no more undone entries in the log for the relevant package | |
596 | foreach ($undoneEntries as $entry) { | |
597 | $this->deleteLog($entry); | |
598 | } | |
599 | WCF::getDB()->commitTransaction(); | |
f6e43f2f MS |
600 | } |
601 | ||
d996e907 MS |
602 | /** |
603 | * Creates a done log entry for the given foreign key. | |
604 | * | |
605 | * @param string $tableName | |
606 | * @param DatabaseTableForeignKey $foreignKey | |
607 | */ | |
608 | protected function createForeignKeyLog($tableName, DatabaseTableForeignKey $foreignKey) { | |
609 | $sql = "INSERT INTO wcf" . WCF_N . "_package_installation_sql_log | |
610 | (packageID, sqlTable, sqlIndex, isDone) | |
611 | VALUES (?, ?, ?, ?)"; | |
612 | $statement = WCF::getDB()->prepareStatement($sql); | |
613 | ||
614 | $statement->execute([ | |
615 | $this->package->packageID, | |
616 | $tableName, | |
617 | $foreignKey->getName(), | |
618 | 1 | |
619 | ]); | |
620 | } | |
621 | ||
f6e43f2f | 622 | /** |
3de2e191 | 623 | * Creates the given table. |
f6e43f2f | 624 | * |
3de2e191 | 625 | * @param DatabaseTable $table |
f6e43f2f | 626 | */ |
3de2e191 | 627 | protected function createTable(DatabaseTable $table) { |
36357b9c AE |
628 | $hasPrimaryKey = false; |
629 | $columnData = array_map(function(IDatabaseTableColumn $column) use (&$hasPrimaryKey) { | |
630 | $data = $column->getData(); | |
631 | if (isset($data['key']) && $data['key'] === 'PRIMARY') { | |
632 | $hasPrimaryKey = true; | |
633 | } | |
634 | ||
3de2e191 | 635 | return [ |
36357b9c | 636 | 'data' => $data, |
3de2e191 | 637 | 'name' => $column->getName() |
f6e43f2f | 638 | ]; |
3de2e191 MS |
639 | }, $table->getColumns()); |
640 | $indexData = array_map(function(DatabaseTableIndex $index) { | |
641 | return [ | |
642 | 'data' => $index->getData(), | |
643 | 'name' => $index->getName() | |
f6e43f2f | 644 | ]; |
3de2e191 MS |
645 | }, $table->getIndices()); |
646 | ||
36357b9c AE |
647 | // Auto columns are implicitly defined as the primary key by MySQL. |
648 | if ($hasPrimaryKey) { | |
649 | $indexData = array_filter($indexData, function($key) { | |
650 | return $key !== 'PRIMARY'; | |
651 | }, ARRAY_FILTER_USE_KEY); | |
652 | } | |
653 | ||
3de2e191 MS |
654 | $this->dbEditor->createTable($table->getName(), $columnData, $indexData); |
655 | ||
656 | foreach ($table->getForeignKeys() as $foreignKey) { | |
dc4b5734 MS |
657 | // Only try to create the foreign key if the referenced database table already exists. |
658 | // If it will be created later on, delay the foreign key creation until after the | |
659 | // referenced table has been created. | |
660 | if ( | |
661 | in_array($foreignKey->getReferencedTable(), $this->existingTableNames) | |
662 | || $foreignKey->getReferencedTable() === $table->getName() | |
663 | ) { | |
664 | $this->dbEditor->addForeignKey($table->getName(), $foreignKey->getName(), $foreignKey->getData()); | |
665 | ||
666 | // foreign keys need to be explicitly logged for proper uninstallation | |
667 | $this->createForeignKeyLog($table->getName(), $foreignKey); | |
668 | } | |
f6e43f2f | 669 | } |
3de2e191 MS |
670 | } |
671 | ||
672 | /** | |
673 | * Deletes the log entry for the given column. | |
674 | * | |
675 | * @param string $tableName | |
676 | * @param IDatabaseTableColumn $column | |
677 | */ | |
678 | protected function deleteColumnLog($tableName, IDatabaseTableColumn $column) { | |
679 | $this->deleteLog(['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]); | |
680 | } | |
681 | ||
682 | /** | |
683 | * Deletes the log entry for the given foreign key. | |
684 | * | |
685 | * @param string $tableName | |
686 | * @param DatabaseTableForeignKey $foreignKey | |
687 | */ | |
688 | protected function deleteForeignKeyLog($tableName, DatabaseTableForeignKey $foreignKey) { | |
689 | $this->deleteLog(['sqlTable' => $tableName, 'sqlIndex' => $foreignKey->getName()]); | |
690 | } | |
691 | ||
692 | /** | |
693 | * Deletes the log entry for the given index. | |
694 | * | |
695 | * @param string $tableName | |
696 | * @param DatabaseTableIndex $index | |
697 | */ | |
698 | protected function deleteIndexLog($tableName, DatabaseTableIndex $index) { | |
699 | $this->deleteLog(['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]); | |
700 | } | |
701 | ||
702 | /** | |
703 | * Deletes a log entry. | |
704 | * | |
705 | * @param array $data | |
706 | */ | |
707 | protected function deleteLog(array $data) { | |
708 | $sql = "DELETE FROM wcf" . WCF_N . "_package_installation_sql_log | |
709 | WHERE packageID = ? | |
710 | AND sqlTable = ? | |
711 | AND sqlColumn = ? | |
712 | AND sqlIndex = ?"; | |
713 | $statement = WCF::getDB()->prepareStatement($sql); | |
714 | ||
715 | $statement->execute([ | |
716 | $this->package->packageID, | |
717 | $data['sqlTable'], | |
718 | $data['sqlColumn'] ?? '', | |
719 | $data['sqlIndex'] ?? '' | |
720 | ]); | |
721 | } | |
722 | ||
723 | /** | |
724 | * Deletes all log entry related to the given table. | |
725 | * | |
726 | * @param DatabaseTable $table | |
727 | */ | |
728 | protected function deleteTableLog(DatabaseTable $table) { | |
729 | $sql = "DELETE FROM wcf" . WCF_N . "_package_installation_sql_log | |
730 | WHERE packageID = ? | |
731 | AND sqlTable = ?"; | |
732 | $statement = WCF::getDB()->prepareStatement($sql); | |
733 | ||
734 | $statement->execute([ | |
735 | $this->package->packageID, | |
736 | $table->getName() | |
737 | ]); | |
738 | } | |
739 | ||
689cb90b MS |
740 | /** |
741 | * Returns `true` if the two columns differ. | |
742 | * | |
743 | * @param IDatabaseTableColumn $oldColumn | |
744 | * @param IDatabaseTableColumn $newColumn | |
745 | * @return bool | |
746 | */ | |
747 | protected function diffColumns(IDatabaseTableColumn $oldColumn, IDatabaseTableColumn $newColumn) { | |
77d58141 MS |
748 | $diff = array_diff($oldColumn->getData(), $newColumn->getData()); |
749 | if (!empty($diff)) { | |
1feb7d64 | 750 | // see https://github.com/WoltLab/WCF/pull/3167 |
77d58141 | 751 | if ( |
1feb7d64 | 752 | array_key_exists('length', $diff) |
77d58141 MS |
753 | && $oldColumn instanceof AbstractIntDatabaseTableColumn |
754 | && ( | |
755 | !($oldColumn instanceof TinyintDatabaseTableColumn) | |
756 | || $oldColumn->getLength() != 1 | |
757 | ) | |
758 | ) { | |
759 | unset($diff['length']); | |
760 | } | |
761 | ||
762 | if (!empty($diff)) { | |
763 | return true; | |
764 | } | |
689cb90b MS |
765 | } |
766 | ||
c519eeea MS |
767 | // default type has to be checked explicitly for `null` to properly detect changing |
768 | // from no default value (`null`) and to an empty string as default value (and vice | |
769 | // versa) | |
770 | if ($oldColumn->getDefaultValue() === null || $newColumn->getDefaultValue() === null) { | |
771 | return $oldColumn->getDefaultValue() !== $newColumn->getDefaultValue(); | |
772 | } | |
773 | ||
774 | // for all other cases, use weak comparison so that `'1'` (from database) and `1` | |
775 | // (from script PIP) match, for example | |
776 | return $oldColumn->getDefaultValue() != $newColumn->getDefaultValue(); | |
689cb90b MS |
777 | } |
778 | ||
c8280fb8 MS |
779 | /** |
780 | * Returns `true` if the two indices differ. | |
781 | * | |
782 | * @param DatabaseTableIndex $oldIndex | |
783 | * @param DatabaseTableIndex $newIndex | |
784 | * @return bool | |
785 | */ | |
786 | protected function diffIndices(DatabaseTableIndex $oldIndex, DatabaseTableIndex $newIndex) { | |
787 | if ($newIndex->hasGeneratedName()) { | |
788 | return !empty(array_diff($oldIndex->getData(), $newIndex->getData())); | |
789 | } | |
790 | ||
791 | return $oldIndex->getName() !== $newIndex->getName(); | |
792 | } | |
793 | ||
3de2e191 MS |
794 | /** |
795 | * Drops the given foreign key. | |
796 | * | |
797 | * @param string $tableName | |
798 | * @param DatabaseTableForeignKey $foreignKey | |
799 | */ | |
800 | protected function dropForeignKey($tableName, DatabaseTableForeignKey $foreignKey) { | |
801 | $this->dbEditor->dropForeignKey($tableName, $foreignKey->getName()); | |
802 | $this->dbEditor->dropIndex($tableName, $foreignKey->getName()); | |
803 | } | |
804 | ||
805 | /** | |
806 | * Drops the given index. | |
807 | * | |
808 | * @param string $tableName | |
809 | * @param DatabaseTableIndex $index | |
810 | */ | |
811 | protected function dropIndex($tableName, DatabaseTableIndex $index) { | |
812 | $this->dbEditor->dropIndex($tableName, $index->getName()); | |
813 | } | |
814 | ||
815 | /** | |
816 | * Drops the given table. | |
817 | * | |
818 | * @param DatabaseTable $table | |
819 | */ | |
820 | protected function dropTable(DatabaseTable $table) { | |
821 | $this->dbEditor->dropTable($table->getName()); | |
822 | } | |
823 | ||
824 | /** | |
825 | * Finalizes the log entry for the creation of the given column. | |
826 | * | |
827 | * @param string $tableName | |
828 | * @param IDatabaseTableColumn $column | |
829 | */ | |
830 | protected function finalizeColumnLog($tableName, IDatabaseTableColumn $column) { | |
831 | $this->finalizeLog(['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]); | |
832 | } | |
833 | ||
834 | /** | |
835 | * Finalizes the log entry for adding the given index. | |
836 | * | |
837 | * @param string $tableName | |
838 | * @param DatabaseTableForeignKey $foreignKey | |
839 | */ | |
840 | protected function finalizeForeignKeyLog($tableName, DatabaseTableForeignKey $foreignKey) { | |
841 | $this->finalizeLog(['sqlTable' => $tableName, 'sqlIndex' => $foreignKey->getName()]); | |
842 | } | |
843 | ||
844 | /** | |
845 | * Finalizes the log entry for adding the given index. | |
846 | * | |
847 | * @param string $tableName | |
848 | * @param DatabaseTableIndex $index | |
849 | */ | |
850 | protected function finalizeIndexLog($tableName, DatabaseTableIndex $index) { | |
851 | $this->finalizeLog(['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]); | |
852 | } | |
853 | ||
854 | /** | |
855 | * Finalizes a log entry after the relevant change has been executed. | |
856 | * | |
857 | * @param array $data | |
858 | */ | |
859 | protected function finalizeLog(array $data) { | |
860 | $sql = "UPDATE wcf" . WCF_N . "_package_installation_sql_log | |
861 | SET isDone = ? | |
862 | WHERE packageID = ? | |
863 | AND sqlTable = ? | |
864 | AND sqlColumn = ? | |
865 | AND sqlIndex = ?"; | |
866 | $statement = WCF::getDB()->prepareStatement($sql); | |
867 | ||
868 | $statement->execute([ | |
869 | 1, | |
870 | $this->package->packageID, | |
871 | $data['sqlTable'], | |
872 | $data['sqlColumn'] ?? '', | |
873 | $data['sqlIndex'] ?? '' | |
874 | ]); | |
875 | } | |
876 | ||
877 | /** | |
878 | * Finalizes the log entry for the creation of the given table. | |
879 | * | |
880 | * @param DatabaseTable $table | |
881 | */ | |
882 | protected function finalizeTableLog(DatabaseTable $table) { | |
883 | $this->finalizeLog(['sqlTable' => $table->getName()]); | |
884 | } | |
885 | ||
886 | /** | |
887 | * Returns the id of the package to with the given column belongs to. If there is no specific | |
888 | * log entry for the given column, the table log is checked and the relevant package id of | |
889 | * the whole table is returned. If the package of the table is also unknown, `null` is returned. | |
890 | * | |
891 | * @param DatabaseTable $table | |
892 | * @param IDatabaseTableColumn $column | |
893 | * @return null|int | |
894 | */ | |
895 | protected function getColumnPackageID(DatabaseTable $table, IDatabaseTableColumn $column) { | |
896 | if (isset($this->columnPackageIDs[$table->getName()][$column->getName()])) { | |
897 | return $this->columnPackageIDs[$table->getName()][$column->getName()]; | |
898 | } | |
899 | else if (isset($this->tablePackageIDs[$table->getName()])) { | |
900 | return $this->tablePackageIDs[$table->getName()]; | |
f6e43f2f MS |
901 | } |
902 | ||
3de2e191 MS |
903 | return null; |
904 | } | |
905 | ||
906 | /** | |
907 | * Returns the `DatabaseTable` object for the table with the given name. | |
908 | * | |
909 | * @param string $tableName | |
910 | * @return DatabaseTable | |
911 | */ | |
912 | protected function getExistingTable($tableName) { | |
913 | if (!isset($this->existingTables[$tableName])) { | |
914 | $this->existingTables[$tableName] = DatabaseTable::createFromExistingTable($this->dbEditor, $tableName); | |
f6e43f2f | 915 | } |
3de2e191 MS |
916 | |
917 | return $this->existingTables[$tableName]; | |
f6e43f2f MS |
918 | } |
919 | ||
920 | /** | |
3de2e191 MS |
921 | * Returns the id of the package to with the given foreign key belongs to. If there is no specific |
922 | * log entry for the given foreign key, the table log is checked and the relevant package id of | |
923 | * the whole table is returned. If the package of the table is also unknown, `null` is returned. | |
f6e43f2f MS |
924 | * |
925 | * @param DatabaseTable $table | |
3de2e191 MS |
926 | * @param DatabaseTableForeignKey $foreignKey |
927 | * @return null|int | |
f6e43f2f | 928 | */ |
3de2e191 MS |
929 | protected function getForeignKeyPackageID(DatabaseTable $table, DatabaseTableForeignKey $foreignKey) { |
930 | if (isset($this->foreignKeyPackageIDs[$table->getName()][$foreignKey->getName()])) { | |
931 | return $this->foreignKeyPackageIDs[$table->getName()][$foreignKey->getName()]; | |
f6e43f2f | 932 | } |
3de2e191 MS |
933 | else if (isset($this->tablePackageIDs[$table->getName()])) { |
934 | return $this->tablePackageIDs[$table->getName()]; | |
f6e43f2f MS |
935 | } |
936 | ||
3de2e191 | 937 | return null; |
f6e43f2f MS |
938 | } |
939 | ||
940 | /** | |
3de2e191 MS |
941 | * Returns the id of the package to with the given index belongs to. If there is no specific |
942 | * log entry for the given index, the table log is checked and the relevant package id of | |
943 | * the whole table is returned. If the package of the table is also unknown, `null` is returned. | |
f6e43f2f MS |
944 | * |
945 | * @param DatabaseTable $table | |
3de2e191 MS |
946 | * @param DatabaseTableIndex $index |
947 | * @return null|int | |
f6e43f2f | 948 | */ |
3de2e191 MS |
949 | protected function getIndexPackageID(DatabaseTable $table, DatabaseTableIndex $index) { |
950 | if (isset($this->indexPackageIDs[$table->getName()][$index->getName()])) { | |
951 | return $this->indexPackageIDs[$table->getName()][$index->getName()]; | |
f6e43f2f | 952 | } |
3de2e191 MS |
953 | else if (isset($this->tablePackageIDs[$table->getName()])) { |
954 | return $this->tablePackageIDs[$table->getName()]; | |
f6e43f2f MS |
955 | } |
956 | ||
3de2e191 MS |
957 | return null; |
958 | } | |
959 | ||
960 | /** | |
961 | * Prepares the log entry for the creation of the given column. | |
962 | * | |
963 | * @param string $tableName | |
964 | * @param IDatabaseTableColumn $column | |
965 | */ | |
966 | protected function prepareColumnLog($tableName, IDatabaseTableColumn $column) { | |
967 | $this->prepareLog(['sqlTable' => $tableName, 'sqlColumn' => $column->getName()]); | |
968 | } | |
969 | ||
970 | /** | |
971 | * Prepares the log entry for adding the given foreign key. | |
972 | * | |
973 | * @param string $tableName | |
974 | * @param DatabaseTableForeignKey $foreignKey | |
975 | */ | |
976 | protected function prepareForeignKeyLog($tableName, DatabaseTableForeignKey $foreignKey) { | |
977 | $this->prepareLog(['sqlTable' => $tableName, 'sqlIndex' => $foreignKey->getName()]); | |
978 | } | |
979 | ||
980 | /** | |
981 | * Prepares the log entry for adding the given index. | |
982 | * | |
983 | * @param string $tableName | |
984 | * @param DatabaseTableIndex $index | |
985 | */ | |
986 | protected function prepareIndexLog($tableName, DatabaseTableIndex $index) { | |
987 | $this->prepareLog(['sqlTable' => $tableName, 'sqlIndex' => $index->getName()]); | |
988 | } | |
989 | ||
990 | /** | |
991 | * Prepares a log entry before the relevant change has been executed. | |
992 | * | |
993 | * @param array $data | |
994 | */ | |
995 | protected function prepareLog(array $data) { | |
996 | $sql = "INSERT INTO wcf" . WCF_N . "_package_installation_sql_log | |
997 | (packageID, sqlTable, sqlColumn, sqlIndex, isDone) | |
998 | VALUES (?, ?, ?, ?, ?)"; | |
999 | $statement = WCF::getDB()->prepareStatement($sql); | |
1000 | ||
1001 | $statement->execute([ | |
1002 | $this->package->packageID, | |
1003 | $data['sqlTable'], | |
1004 | $data['sqlColumn'] ?? '', | |
1005 | $data['sqlIndex'] ?? '', | |
1006 | 0 | |
1007 | ]); | |
1008 | } | |
1009 | ||
1010 | /** | |
1011 | * Prepares the log entry for the creation of the given table. | |
1012 | * | |
1013 | * @param DatabaseTable $table | |
1014 | */ | |
1015 | protected function prepareTableLog(DatabaseTable $table) { | |
1016 | $this->prepareLog(['sqlTable' => $table->getName()]); | |
1017 | } | |
1018 | ||
1019 | /** | |
1020 | * Processes all tables and updates the current table layouts to match the specified layouts. | |
1021 | * | |
1022 | * @throws \RuntimeException if validation of the required layout changes fails | |
1023 | */ | |
1024 | public function process() { | |
1025 | $this->checkPendingLogEntries(); | |
1026 | ||
1027 | $errors = $this->validate(); | |
1028 | if (!empty($errors)) { | |
1029 | throw new \RuntimeException(WCF::getLanguage()->getDynamicVariable('wcf.acp.package.error.databaseChange', [ | |
1030 | 'errors' => $errors | |
1031 | ])); | |
f6e43f2f | 1032 | } |
3de2e191 MS |
1033 | |
1034 | $this->calculateChanges(); | |
1035 | ||
1036 | $this->applyChanges(); | |
f6e43f2f MS |
1037 | } |
1038 | ||
1039 | /** | |
1040 | * Checks if the relevant table layout changes can be executed and returns an array with information | |
3de2e191 | 1041 | * on all validation errors. |
f6e43f2f MS |
1042 | * |
1043 | * @return array | |
1044 | */ | |
1045 | public function validate() { | |
1046 | $errors = []; | |
1047 | foreach ($this->tables as $table) { | |
1048 | if ($table->willBeDropped()) { | |
1049 | if (in_array($table->getName(), $this->existingTableNames)) { | |
1050 | if (!isset($this->tablePackageIDs[$table->getName()])) { | |
1051 | $errors[] = [ | |
1052 | 'tableName' => $table->getName(), | |
1053 | 'type' => 'unregisteredTableDrop' | |
1054 | ]; | |
1055 | } | |
1056 | else if ($this->tablePackageIDs[$table->getName()] !== $this->package->packageID) { | |
1057 | $errors[] = [ | |
1058 | 'tableName' => $table->getName(), | |
1059 | 'type' => 'foreignTableDrop' | |
1060 | ]; | |
1061 | } | |
1062 | } | |
1063 | } | |
f9515e9f | 1064 | else { |
1a922b48 | 1065 | $existingTable = null; |
f9515e9f MS |
1066 | if (in_array($table->getName(), $this->existingTableNames)) { |
1067 | if (!isset($this->tablePackageIDs[$table->getName()])) { | |
1068 | $errors[] = [ | |
1069 | 'tableName' => $table->getName(), | |
1070 | 'type' => 'unregisteredTableChange', | |
1071 | ]; | |
1072 | } | |
1073 | else { | |
1074 | $existingTable = DatabaseTable::createFromExistingTable($this->dbEditor, $table->getName()); | |
1075 | $existingColumns = $existingTable->getColumns(); | |
1076 | $existingIndices = $existingTable->getIndices(); | |
1077 | $existingForeignKeys = $existingTable->getForeignKeys(); | |
1078 | ||
1079 | foreach ($table->getColumns() as $column) { | |
1080 | if (isset($existingColumns[$column->getName()])) { | |
1081 | $columnPackageID = $this->getColumnPackageID($table, $column); | |
1082 | if ($column->willBeDropped()) { | |
1083 | if ($columnPackageID !== $this->package->packageID) { | |
1084 | $errors[] = [ | |
1085 | 'columnName' => $column->getName(), | |
1086 | 'tableName' => $table->getName(), | |
1087 | 'type' => 'foreignColumnDrop', | |
1088 | ]; | |
1089 | } | |
1090 | } | |
1091 | else if ($columnPackageID !== $this->package->packageID) { | |
f6e43f2f MS |
1092 | $errors[] = [ |
1093 | 'columnName' => $column->getName(), | |
1094 | 'tableName' => $table->getName(), | |
f9515e9f | 1095 | 'type' => 'foreignColumnChange', |
f6e43f2f MS |
1096 | ]; |
1097 | } | |
1098 | } | |
f6e43f2f | 1099 | } |
f9515e9f MS |
1100 | |
1101 | foreach ($table->getIndices() as $index) { | |
1102 | foreach ($existingIndices as $existingIndex) { | |
1103 | if (empty(array_diff($index->getData(), $existingIndex->getData()))) { | |
1104 | if ($index->willBeDropped()) { | |
1105 | if ($this->getIndexPackageID($table, $index) !== $this->package->packageID) { | |
1106 | $errors[] = [ | |
1107 | 'columnNames' => implode(',', $existingIndex->getColumns()), | |
1108 | 'tableName' => $table->getName(), | |
1109 | 'type' => 'foreignIndexDrop', | |
1110 | ]; | |
1111 | } | |
f6e43f2f | 1112 | } |
f9515e9f MS |
1113 | |
1114 | continue 2; | |
f6e43f2f | 1115 | } |
f6e43f2f MS |
1116 | } |
1117 | } | |
f9515e9f MS |
1118 | |
1119 | foreach ($table->getForeignKeys() as $foreignKey) { | |
1120 | foreach ($existingForeignKeys as $existingForeignKey) { | |
1121 | if (empty(array_diff($foreignKey->getData(), $existingForeignKey->getData()))) { | |
1122 | if ($foreignKey->willBeDropped()) { | |
1123 | if ($this->getForeignKeyPackageID($table, $foreignKey) !== $this->package->packageID) { | |
1124 | $errors[] = [ | |
1125 | 'columnNames' => implode(',', $existingForeignKey->getColumns()), | |
1126 | 'tableName' => $table->getName(), | |
1127 | 'type' => 'foreignForeignKeyDrop', | |
1128 | ]; | |
1129 | } | |
f6e43f2f | 1130 | } |
f9515e9f MS |
1131 | |
1132 | continue 2; | |
f6e43f2f | 1133 | } |
f9515e9f MS |
1134 | } |
1135 | } | |
1136 | } | |
1137 | } | |
1138 | ||
1139 | foreach ($table->getIndices() as $index) { | |
0a2e3270 MS |
1140 | foreach ($index->getColumns() as $indexColumn) { |
1141 | $column = $this->getColumnByName($indexColumn, $table, $existingTable); | |
1142 | if ($column === null) { | |
1143 | if (!$index->willBeDropped()) { | |
f9515e9f MS |
1144 | $errors[] = [ |
1145 | 'columnName' => $indexColumn, | |
1146 | 'columnNames' => implode(',', $index->getColumns()), | |
1147 | 'tableName' => $table->getName(), | |
0a2e3270 | 1148 | 'type' => 'nonexistingColumnInIndex', |
f9515e9f | 1149 | ]; |
f6e43f2f MS |
1150 | } |
1151 | } | |
0a2e3270 MS |
1152 | else if ( |
1153 | $index->getType() === DatabaseTableIndex::PRIMARY_TYPE | |
1154 | && !$index->willBeDropped() | |
1155 | && !$column->isNotNull() | |
1156 | ) { | |
1157 | $errors[] = [ | |
1158 | 'columnName' => $indexColumn, | |
1159 | 'columnNames' => implode(',', $index->getColumns()), | |
1160 | 'tableName' => $table->getName(), | |
1161 | 'type' => 'nullColumnInPrimaryIndex', | |
1162 | ]; | |
1163 | } | |
f6e43f2f MS |
1164 | } |
1165 | } | |
998e9224 MS |
1166 | |
1167 | foreach ($table->getForeignKeys() as $foreignKey) { | |
1168 | $referencedTableExists = in_array($foreignKey->getReferencedTable(), $this->existingTableNames); | |
1169 | foreach ($this->tables as $processedTable) { | |
1170 | if ($processedTable->getName() === $foreignKey->getReferencedTable()) { | |
1171 | $referencedTableExists = !$processedTable->willBeDropped(); | |
1172 | } | |
1173 | } | |
1174 | ||
1175 | if (!$referencedTableExists) { | |
1176 | $errors[] = [ | |
1177 | 'columnNames' => implode(',', $foreignKey->getColumns()), | |
1178 | 'referencedTableName' => $foreignKey->getReferencedTable(), | |
1179 | 'tableName' => $table->getName(), | |
1180 | 'type' => 'unknownTableInForeignKey', | |
1181 | ]; | |
1182 | } | |
1183 | } | |
f6e43f2f MS |
1184 | } |
1185 | } | |
1186 | ||
1187 | return $errors; | |
1188 | } | |
f9515e9f MS |
1189 | |
1190 | /** | |
1191 | * Returns the column with the given name from the given table. | |
1192 | * | |
1193 | * @param string $columnName | |
1194 | * @param DatabaseTable $updateTable | |
1195 | * @param DatabaseTable|null $existingTable | |
1196 | * @return IDatabaseTableColumn|null | |
1197 | * @since 5.2.10 | |
1198 | */ | |
1199 | protected function getColumnByName($columnName, DatabaseTable $updateTable, DatabaseTable $existingTable = null) { | |
1200 | foreach ($updateTable->getColumns() as $column) { | |
7c8a1b33 | 1201 | if ($column->getName() === $columnName) { |
f9515e9f MS |
1202 | return $column; |
1203 | } | |
1204 | } | |
1205 | ||
2834fb0a MS |
1206 | if ($existingTable) { |
1207 | foreach ($existingTable->getColumns() as $column) { | |
1208 | if ($column->getName() === $columnName) { | |
1209 | return $column; | |
1210 | } | |
f9515e9f MS |
1211 | } |
1212 | } | |
1213 | ||
1214 | return null; | |
1215 | } | |
f6e43f2f | 1216 | } |