Exception
2 years ago
Visitor
9 months ago
AbstractAsset.php
2 years ago
AbstractSchemaManager.php
9 months ago
Column.php
2 years ago
ColumnDiff.php
2 years ago
Comparator.php
2 years ago
Constraint.php
2 years ago
DB2SchemaManager.php
2 years ago
DefaultSchemaManagerFactory.php
2 years ago
ForeignKeyConstraint.php
9 months ago
Identifier.php
2 years ago
Index.php
2 years ago
LegacySchemaManagerFactory.php
2 years ago
MySQLSchemaManager.php
9 months ago
OracleSchemaManager.php
2 years ago
PostgreSQLSchemaManager.php
9 months ago
SQLServerSchemaManager.php
9 months ago
Schema.php
2 years ago
SchemaConfig.php
2 years ago
SchemaDiff.php
2 years ago
SchemaException.php
2 years ago
SchemaManagerFactory.php
2 years ago
Sequence.php
2 years ago
SqliteSchemaManager.php
8 months ago
Table.php
2 years ago
TableDiff.php
2 years ago
UniqueConstraint.php
2 years ago
View.php
2 years ago
Comparator.php
411 lines
| 1 | <?php |
| 2 | namespace MailPoetVendor\Doctrine\DBAL\Schema; |
| 3 | if (!defined('ABSPATH')) exit; |
| 4 | use BadMethodCallException; |
| 5 | use MailPoetVendor\Doctrine\DBAL\Exception; |
| 6 | use MailPoetVendor\Doctrine\DBAL\Platforms\AbstractPlatform; |
| 7 | use MailPoetVendor\Doctrine\DBAL\Types; |
| 8 | use MailPoetVendor\Doctrine\Deprecations\Deprecation; |
| 9 | use function array_intersect_key; |
| 10 | use function array_key_exists; |
| 11 | use function array_keys; |
| 12 | use function array_map; |
| 13 | use function array_merge; |
| 14 | use function array_unique; |
| 15 | use function assert; |
| 16 | use function count; |
| 17 | use function get_class; |
| 18 | use function sprintf; |
| 19 | use function strtolower; |
| 20 | class Comparator |
| 21 | { |
| 22 | private ?AbstractPlatform $platform; |
| 23 | public function __construct(?AbstractPlatform $platform = null) |
| 24 | { |
| 25 | if ($platform === null) { |
| 26 | Deprecation::triggerIfCalledFromOutside('doctrine/dbal', 'https://github.com/doctrine/dbal/pull/4746', 'Not passing a $platform to %s is deprecated.' . ' Use AbstractSchemaManager::createComparator() to instantiate the comparator.', __METHOD__); |
| 27 | } |
| 28 | $this->platform = $platform; |
| 29 | } |
| 30 | public function __call(string $method, array $args) : SchemaDiff |
| 31 | { |
| 32 | if ($method !== 'compareSchemas') { |
| 33 | throw new BadMethodCallException(sprintf('Unknown method "%s"', $method)); |
| 34 | } |
| 35 | return $this->doCompareSchemas(...$args); |
| 36 | } |
| 37 | public static function __callStatic(string $method, array $args) : SchemaDiff |
| 38 | { |
| 39 | if ($method !== 'compareSchemas') { |
| 40 | throw new BadMethodCallException(sprintf('Unknown method "%s"', $method)); |
| 41 | } |
| 42 | Deprecation::trigger('doctrine/dbal', 'https://github.com/doctrine/dbal/pull/4707', 'Calling %s::%s() statically is deprecated.', self::class, $method); |
| 43 | $comparator = new self(); |
| 44 | return $comparator->doCompareSchemas(...$args); |
| 45 | } |
| 46 | private function doCompareSchemas(Schema $fromSchema, Schema $toSchema) |
| 47 | { |
| 48 | $createdSchemas = []; |
| 49 | $droppedSchemas = []; |
| 50 | $createdTables = []; |
| 51 | $alteredTables = []; |
| 52 | $droppedTables = []; |
| 53 | $createdSequences = []; |
| 54 | $alteredSequences = []; |
| 55 | $droppedSequences = []; |
| 56 | $orphanedForeignKeys = []; |
| 57 | $foreignKeysToTable = []; |
| 58 | foreach ($toSchema->getNamespaces() as $namespace) { |
| 59 | if ($fromSchema->hasNamespace($namespace)) { |
| 60 | continue; |
| 61 | } |
| 62 | $createdSchemas[$namespace] = $namespace; |
| 63 | } |
| 64 | foreach ($fromSchema->getNamespaces() as $namespace) { |
| 65 | if ($toSchema->hasNamespace($namespace)) { |
| 66 | continue; |
| 67 | } |
| 68 | $droppedSchemas[$namespace] = $namespace; |
| 69 | } |
| 70 | foreach ($toSchema->getTables() as $table) { |
| 71 | $tableName = $table->getShortestName($toSchema->getName()); |
| 72 | if (!$fromSchema->hasTable($tableName)) { |
| 73 | $createdTables[$tableName] = $toSchema->getTable($tableName); |
| 74 | } else { |
| 75 | $tableDifferences = $this->diffTable($fromSchema->getTable($tableName), $toSchema->getTable($tableName)); |
| 76 | if ($tableDifferences !== \false) { |
| 77 | $alteredTables[$tableName] = $tableDifferences; |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | foreach ($fromSchema->getTables() as $table) { |
| 82 | $tableName = $table->getShortestName($fromSchema->getName()); |
| 83 | $table = $fromSchema->getTable($tableName); |
| 84 | if (!$toSchema->hasTable($tableName)) { |
| 85 | $droppedTables[$tableName] = $table; |
| 86 | } |
| 87 | // also remember all foreign keys that point to a specific table |
| 88 | foreach ($table->getForeignKeys() as $foreignKey) { |
| 89 | $foreignTable = strtolower($foreignKey->getForeignTableName()); |
| 90 | if (!isset($foreignKeysToTable[$foreignTable])) { |
| 91 | $foreignKeysToTable[$foreignTable] = []; |
| 92 | } |
| 93 | $foreignKeysToTable[$foreignTable][] = $foreignKey; |
| 94 | } |
| 95 | } |
| 96 | foreach ($droppedTables as $tableName => $table) { |
| 97 | if (!isset($foreignKeysToTable[$tableName])) { |
| 98 | continue; |
| 99 | } |
| 100 | foreach ($foreignKeysToTable[$tableName] as $foreignKey) { |
| 101 | if (isset($droppedTables[strtolower($foreignKey->getLocalTableName())])) { |
| 102 | continue; |
| 103 | } |
| 104 | $orphanedForeignKeys[] = $foreignKey; |
| 105 | } |
| 106 | // deleting duplicated foreign keys present on both on the orphanedForeignKey |
| 107 | // and the removedForeignKeys from changedTables |
| 108 | foreach ($foreignKeysToTable[$tableName] as $foreignKey) { |
| 109 | // strtolower the table name to make if compatible with getShortestName |
| 110 | $localTableName = strtolower($foreignKey->getLocalTableName()); |
| 111 | if (!isset($alteredTables[$localTableName])) { |
| 112 | continue; |
| 113 | } |
| 114 | foreach ($alteredTables[$localTableName]->getDroppedForeignKeys() as $droppedForeignKey) { |
| 115 | assert($droppedForeignKey instanceof ForeignKeyConstraint); |
| 116 | // We check if the key is from the removed table if not we skip. |
| 117 | if ($tableName !== strtolower($droppedForeignKey->getForeignTableName())) { |
| 118 | continue; |
| 119 | } |
| 120 | $alteredTables[$localTableName]->unsetDroppedForeignKey($droppedForeignKey); |
| 121 | } |
| 122 | } |
| 123 | } |
| 124 | foreach ($toSchema->getSequences() as $sequence) { |
| 125 | $sequenceName = $sequence->getShortestName($toSchema->getName()); |
| 126 | if (!$fromSchema->hasSequence($sequenceName)) { |
| 127 | if (!$this->isAutoIncrementSequenceInSchema($fromSchema, $sequence)) { |
| 128 | $createdSequences[] = $sequence; |
| 129 | } |
| 130 | } else { |
| 131 | if ($this->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) { |
| 132 | $alteredSequences[] = $toSchema->getSequence($sequenceName); |
| 133 | } |
| 134 | } |
| 135 | } |
| 136 | foreach ($fromSchema->getSequences() as $sequence) { |
| 137 | if ($this->isAutoIncrementSequenceInSchema($toSchema, $sequence)) { |
| 138 | continue; |
| 139 | } |
| 140 | $sequenceName = $sequence->getShortestName($fromSchema->getName()); |
| 141 | if ($toSchema->hasSequence($sequenceName)) { |
| 142 | continue; |
| 143 | } |
| 144 | $droppedSequences[] = $sequence; |
| 145 | } |
| 146 | $diff = new SchemaDiff($createdTables, $alteredTables, $droppedTables, $fromSchema, $createdSchemas, $droppedSchemas, $createdSequences, $alteredSequences, $droppedSequences); |
| 147 | $diff->orphanedForeignKeys = $orphanedForeignKeys; |
| 148 | return $diff; |
| 149 | } |
| 150 | public function compare(Schema $fromSchema, Schema $toSchema) |
| 151 | { |
| 152 | Deprecation::trigger('doctrine/dbal', 'https://github.com/doctrine/dbal/pull/4707', 'Method compare() is deprecated. Use a non-static call to compareSchemas() instead.'); |
| 153 | return $this->compareSchemas($fromSchema, $toSchema); |
| 154 | } |
| 155 | private function isAutoIncrementSequenceInSchema($schema, $sequence) : bool |
| 156 | { |
| 157 | foreach ($schema->getTables() as $table) { |
| 158 | if ($sequence->isAutoIncrementsFor($table)) { |
| 159 | return \true; |
| 160 | } |
| 161 | } |
| 162 | return \false; |
| 163 | } |
| 164 | public function diffSequence(Sequence $sequence1, Sequence $sequence2) |
| 165 | { |
| 166 | if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) { |
| 167 | return \true; |
| 168 | } |
| 169 | return $sequence1->getInitialValue() !== $sequence2->getInitialValue(); |
| 170 | } |
| 171 | public function diffTable(Table $fromTable, Table $toTable) |
| 172 | { |
| 173 | Deprecation::triggerIfCalledFromOutside('doctrine/dbal', 'https://github.com/doctrine/dbal/pull/5770', '%s is deprecated. Use compareTables() instead.', __METHOD__); |
| 174 | $diff = $this->compareTables($fromTable, $toTable); |
| 175 | if ($diff->isEmpty()) { |
| 176 | return \false; |
| 177 | } |
| 178 | return $diff; |
| 179 | } |
| 180 | public function compareTables(Table $fromTable, Table $toTable) : TableDiff |
| 181 | { |
| 182 | $addedColumns = []; |
| 183 | $modifiedColumns = []; |
| 184 | $droppedColumns = []; |
| 185 | $addedIndexes = []; |
| 186 | $modifiedIndexes = []; |
| 187 | $droppedIndexes = []; |
| 188 | $addedForeignKeys = []; |
| 189 | $modifiedForeignKeys = []; |
| 190 | $droppedForeignKeys = []; |
| 191 | $fromTableColumns = $fromTable->getColumns(); |
| 192 | $toTableColumns = $toTable->getColumns(); |
| 193 | foreach ($toTableColumns as $columnName => $column) { |
| 194 | if ($fromTable->hasColumn($columnName)) { |
| 195 | continue; |
| 196 | } |
| 197 | $addedColumns[$columnName] = $column; |
| 198 | } |
| 199 | foreach ($fromTableColumns as $columnName => $column) { |
| 200 | // See if column is removed in "to" table. |
| 201 | if (!$toTable->hasColumn($columnName)) { |
| 202 | $droppedColumns[$columnName] = $column; |
| 203 | continue; |
| 204 | } |
| 205 | $toColumn = $toTable->getColumn($columnName); |
| 206 | // See if column has changed properties in "to" table. |
| 207 | $changedProperties = $this->diffColumn($column, $toColumn); |
| 208 | if ($this->platform !== null) { |
| 209 | if ($this->columnsEqual($column, $toColumn)) { |
| 210 | continue; |
| 211 | } |
| 212 | } elseif (count($changedProperties) === 0) { |
| 213 | continue; |
| 214 | } |
| 215 | $modifiedColumns[$column->getName()] = new ColumnDiff($column->getName(), $toColumn, $changedProperties, $column); |
| 216 | } |
| 217 | $renamedColumns = $this->detectRenamedColumns($addedColumns, $droppedColumns); |
| 218 | $fromTableIndexes = $fromTable->getIndexes(); |
| 219 | $toTableIndexes = $toTable->getIndexes(); |
| 220 | foreach ($toTableIndexes as $indexName => $index) { |
| 221 | if ($index->isPrimary() && $fromTable->getPrimaryKey() !== null || $fromTable->hasIndex($indexName)) { |
| 222 | continue; |
| 223 | } |
| 224 | $addedIndexes[$indexName] = $index; |
| 225 | } |
| 226 | foreach ($fromTableIndexes as $indexName => $index) { |
| 227 | // See if index is removed in "to" table. |
| 228 | if ($index->isPrimary() && $toTable->getPrimaryKey() === null || !$index->isPrimary() && !$toTable->hasIndex($indexName)) { |
| 229 | $droppedIndexes[$indexName] = $index; |
| 230 | continue; |
| 231 | } |
| 232 | // See if index has changed in "to" table. |
| 233 | $toTableIndex = $index->isPrimary() ? $toTable->getPrimaryKey() : $toTable->getIndex($indexName); |
| 234 | assert($toTableIndex instanceof Index); |
| 235 | if (!$this->diffIndex($index, $toTableIndex)) { |
| 236 | continue; |
| 237 | } |
| 238 | $modifiedIndexes[$indexName] = $toTableIndex; |
| 239 | } |
| 240 | $renamedIndexes = $this->detectRenamedIndexes($addedIndexes, $droppedIndexes); |
| 241 | $fromForeignKeys = $fromTable->getForeignKeys(); |
| 242 | $toForeignKeys = $toTable->getForeignKeys(); |
| 243 | foreach ($fromForeignKeys as $fromKey => $fromConstraint) { |
| 244 | foreach ($toForeignKeys as $toKey => $toConstraint) { |
| 245 | if ($this->diffForeignKey($fromConstraint, $toConstraint) === \false) { |
| 246 | unset($fromForeignKeys[$fromKey], $toForeignKeys[$toKey]); |
| 247 | } else { |
| 248 | if (strtolower($fromConstraint->getName()) === strtolower($toConstraint->getName())) { |
| 249 | $modifiedForeignKeys[] = $toConstraint; |
| 250 | unset($fromForeignKeys[$fromKey], $toForeignKeys[$toKey]); |
| 251 | } |
| 252 | } |
| 253 | } |
| 254 | } |
| 255 | foreach ($fromForeignKeys as $fromConstraint) { |
| 256 | $droppedForeignKeys[] = $fromConstraint; |
| 257 | } |
| 258 | foreach ($toForeignKeys as $toConstraint) { |
| 259 | $addedForeignKeys[] = $toConstraint; |
| 260 | } |
| 261 | return new TableDiff($toTable->getName(), $addedColumns, $modifiedColumns, $droppedColumns, $addedIndexes, $modifiedIndexes, $droppedIndexes, $fromTable, $addedForeignKeys, $modifiedForeignKeys, $droppedForeignKeys, $renamedColumns, $renamedIndexes); |
| 262 | } |
| 263 | private function detectRenamedColumns(array &$addedColumns, array &$removedColumns) : array |
| 264 | { |
| 265 | $candidatesByName = []; |
| 266 | foreach ($addedColumns as $addedColumnName => $addedColumn) { |
| 267 | foreach ($removedColumns as $removedColumn) { |
| 268 | if (!$this->columnsEqual($addedColumn, $removedColumn)) { |
| 269 | continue; |
| 270 | } |
| 271 | $candidatesByName[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName]; |
| 272 | } |
| 273 | } |
| 274 | $renamedColumns = []; |
| 275 | foreach ($candidatesByName as $candidates) { |
| 276 | if (count($candidates) !== 1) { |
| 277 | continue; |
| 278 | } |
| 279 | [$removedColumn, $addedColumn] = $candidates[0]; |
| 280 | $removedColumnName = $removedColumn->getName(); |
| 281 | $addedColumnName = strtolower($addedColumn->getName()); |
| 282 | if (isset($renamedColumns[$removedColumnName])) { |
| 283 | continue; |
| 284 | } |
| 285 | $renamedColumns[$removedColumnName] = $addedColumn; |
| 286 | unset($addedColumns[$addedColumnName], $removedColumns[strtolower($removedColumnName)]); |
| 287 | } |
| 288 | return $renamedColumns; |
| 289 | } |
| 290 | private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndexes) : array |
| 291 | { |
| 292 | $candidatesByName = []; |
| 293 | // Gather possible rename candidates by comparing each added and removed index based on semantics. |
| 294 | foreach ($addedIndexes as $addedIndexName => $addedIndex) { |
| 295 | foreach ($removedIndexes as $removedIndex) { |
| 296 | if ($this->diffIndex($addedIndex, $removedIndex)) { |
| 297 | continue; |
| 298 | } |
| 299 | $candidatesByName[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName]; |
| 300 | } |
| 301 | } |
| 302 | $renamedIndexes = []; |
| 303 | foreach ($candidatesByName as $candidates) { |
| 304 | // If the current rename candidate contains exactly one semantically equal index, |
| 305 | // we can safely rename it. |
| 306 | // Otherwise, it is unclear if a rename action is really intended, |
| 307 | // therefore we let those ambiguous indexes be added/dropped. |
| 308 | if (count($candidates) !== 1) { |
| 309 | continue; |
| 310 | } |
| 311 | [$removedIndex, $addedIndex] = $candidates[0]; |
| 312 | $removedIndexName = strtolower($removedIndex->getName()); |
| 313 | $addedIndexName = strtolower($addedIndex->getName()); |
| 314 | if (isset($renamedIndexes[$removedIndexName])) { |
| 315 | continue; |
| 316 | } |
| 317 | $renamedIndexes[$removedIndexName] = $addedIndex; |
| 318 | unset($addedIndexes[$addedIndexName], $removedIndexes[$removedIndexName]); |
| 319 | } |
| 320 | return $renamedIndexes; |
| 321 | } |
| 322 | public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2) |
| 323 | { |
| 324 | if (array_map('strtolower', $key1->getUnquotedLocalColumns()) !== array_map('strtolower', $key2->getUnquotedLocalColumns())) { |
| 325 | return \true; |
| 326 | } |
| 327 | if (array_map('strtolower', $key1->getUnquotedForeignColumns()) !== array_map('strtolower', $key2->getUnquotedForeignColumns())) { |
| 328 | return \true; |
| 329 | } |
| 330 | if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) { |
| 331 | return \true; |
| 332 | } |
| 333 | if ($key1->onUpdate() !== $key2->onUpdate()) { |
| 334 | return \true; |
| 335 | } |
| 336 | return $key1->onDelete() !== $key2->onDelete(); |
| 337 | } |
| 338 | public function columnsEqual(Column $column1, Column $column2) : bool |
| 339 | { |
| 340 | if ($this->platform === null) { |
| 341 | return $this->diffColumn($column1, $column2) === []; |
| 342 | } |
| 343 | return $this->platform->columnsEqual($column1, $column2); |
| 344 | } |
| 345 | public function diffColumn(Column $column1, Column $column2) |
| 346 | { |
| 347 | Deprecation::triggerIfCalledFromOutside('doctrine/dbal', 'https://github.com/doctrine/dbal/pull/5650', '%s is deprecated. Use diffTable() instead.', __METHOD__); |
| 348 | $properties1 = $column1->toArray(); |
| 349 | $properties2 = $column2->toArray(); |
| 350 | $changedProperties = []; |
| 351 | if (get_class($properties1['type']) !== get_class($properties2['type'])) { |
| 352 | $changedProperties[] = 'type'; |
| 353 | } |
| 354 | foreach (['notnull', 'unsigned', 'autoincrement'] as $property) { |
| 355 | if ($properties1[$property] === $properties2[$property]) { |
| 356 | continue; |
| 357 | } |
| 358 | $changedProperties[] = $property; |
| 359 | } |
| 360 | // Null values need to be checked additionally as they tell whether to create or drop a default value. |
| 361 | // null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation. |
| 362 | if (($properties1['default'] === null) !== ($properties2['default'] === null) || $properties1['default'] != $properties2['default']) { |
| 363 | $changedProperties[] = 'default'; |
| 364 | } |
| 365 | if ($properties1['type'] instanceof Types\StringType && !$properties1['type'] instanceof Types\GuidType || $properties1['type'] instanceof Types\BinaryType) { |
| 366 | // check if value of length is set at all, default value assumed otherwise. |
| 367 | $length1 = $properties1['length'] ?? 255; |
| 368 | $length2 = $properties2['length'] ?? 255; |
| 369 | if ($length1 !== $length2) { |
| 370 | $changedProperties[] = 'length'; |
| 371 | } |
| 372 | if ($properties1['fixed'] !== $properties2['fixed']) { |
| 373 | $changedProperties[] = 'fixed'; |
| 374 | } |
| 375 | } elseif ($properties1['type'] instanceof Types\DecimalType) { |
| 376 | if (($properties1['precision'] ?? 10) !== ($properties2['precision'] ?? 10)) { |
| 377 | $changedProperties[] = 'precision'; |
| 378 | } |
| 379 | if ($properties1['scale'] !== $properties2['scale']) { |
| 380 | $changedProperties[] = 'scale'; |
| 381 | } |
| 382 | } |
| 383 | // A null value and an empty string are actually equal for a comment so they should not trigger a change. |
| 384 | if ($properties1['comment'] !== $properties2['comment'] && !($properties1['comment'] === null && $properties2['comment'] === '') && !($properties2['comment'] === null && $properties1['comment'] === '')) { |
| 385 | $changedProperties[] = 'comment'; |
| 386 | } |
| 387 | $customOptions1 = $column1->getCustomSchemaOptions(); |
| 388 | $customOptions2 = $column2->getCustomSchemaOptions(); |
| 389 | foreach (array_merge(array_keys($customOptions1), array_keys($customOptions2)) as $key) { |
| 390 | if (!array_key_exists($key, $properties1) || !array_key_exists($key, $properties2)) { |
| 391 | $changedProperties[] = $key; |
| 392 | } elseif ($properties1[$key] !== $properties2[$key]) { |
| 393 | $changedProperties[] = $key; |
| 394 | } |
| 395 | } |
| 396 | $platformOptions1 = $column1->getPlatformOptions(); |
| 397 | $platformOptions2 = $column2->getPlatformOptions(); |
| 398 | foreach (array_keys(array_intersect_key($platformOptions1, $platformOptions2)) as $key) { |
| 399 | if ($properties1[$key] === $properties2[$key]) { |
| 400 | continue; |
| 401 | } |
| 402 | $changedProperties[] = $key; |
| 403 | } |
| 404 | return array_unique($changedProperties); |
| 405 | } |
| 406 | public function diffIndex(Index $index1, Index $index2) |
| 407 | { |
| 408 | return !($index1->isFulfilledBy($index2) && $index2->isFulfilledBy($index1)); |
| 409 | } |
| 410 | } |
| 411 |