summaryrefslogtreecommitdiff
path: root/vendor/doctrine/dbal/src/Schema/Comparator.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/dbal/src/Schema/Comparator.php')
-rw-r--r--vendor/doctrine/dbal/src/Schema/Comparator.php417
1 files changed, 417 insertions, 0 deletions
diff --git a/vendor/doctrine/dbal/src/Schema/Comparator.php b/vendor/doctrine/dbal/src/Schema/Comparator.php
new file mode 100644
index 0000000..4c60c07
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Comparator.php
@@ -0,0 +1,417 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8
9use function array_map;
10use function assert;
11use function count;
12use function strtolower;
13
14/**
15 * Compares two Schemas and return an instance of SchemaDiff.
16 */
17class Comparator
18{
19 /** @internal The comparator can be only instantiated by a schema manager. */
20 public function __construct(private readonly AbstractPlatform $platform)
21 {
22 }
23
24 /**
25 * Returns the differences between the schemas.
26 */
27 public function compareSchemas(Schema $oldSchema, Schema $newSchema): SchemaDiff
28 {
29 $createdSchemas = [];
30 $droppedSchemas = [];
31 $createdTables = [];
32 $alteredTables = [];
33 $droppedTables = [];
34 $createdSequences = [];
35 $alteredSequences = [];
36 $droppedSequences = [];
37
38 foreach ($newSchema->getNamespaces() as $newNamespace) {
39 if ($oldSchema->hasNamespace($newNamespace)) {
40 continue;
41 }
42
43 $createdSchemas[] = $newNamespace;
44 }
45
46 foreach ($oldSchema->getNamespaces() as $oldNamespace) {
47 if ($newSchema->hasNamespace($oldNamespace)) {
48 continue;
49 }
50
51 $droppedSchemas[] = $oldNamespace;
52 }
53
54 foreach ($newSchema->getTables() as $newTable) {
55 $newTableName = $newTable->getShortestName($newSchema->getName());
56 if (! $oldSchema->hasTable($newTableName)) {
57 $createdTables[] = $newSchema->getTable($newTableName);
58 } else {
59 $tableDiff = $this->compareTables(
60 $oldSchema->getTable($newTableName),
61 $newSchema->getTable($newTableName),
62 );
63
64 if (! $tableDiff->isEmpty()) {
65 $alteredTables[] = $tableDiff;
66 }
67 }
68 }
69
70 // Check if there are tables removed
71 foreach ($oldSchema->getTables() as $oldTable) {
72 $oldTableName = $oldTable->getShortestName($oldSchema->getName());
73
74 $oldTable = $oldSchema->getTable($oldTableName);
75 if ($newSchema->hasTable($oldTableName)) {
76 continue;
77 }
78
79 $droppedTables[] = $oldTable;
80 }
81
82 foreach ($newSchema->getSequences() as $newSequence) {
83 $newSequenceName = $newSequence->getShortestName($newSchema->getName());
84 if (! $oldSchema->hasSequence($newSequenceName)) {
85 if (! $this->isAutoIncrementSequenceInSchema($oldSchema, $newSequence)) {
86 $createdSequences[] = $newSequence;
87 }
88 } else {
89 if ($this->diffSequence($newSequence, $oldSchema->getSequence($newSequenceName))) {
90 $alteredSequences[] = $newSchema->getSequence($newSequenceName);
91 }
92 }
93 }
94
95 foreach ($oldSchema->getSequences() as $oldSequence) {
96 if ($this->isAutoIncrementSequenceInSchema($newSchema, $oldSequence)) {
97 continue;
98 }
99
100 $oldSequenceName = $oldSequence->getShortestName($oldSchema->getName());
101
102 if ($newSchema->hasSequence($oldSequenceName)) {
103 continue;
104 }
105
106 $droppedSequences[] = $oldSequence;
107 }
108
109 return new SchemaDiff(
110 $createdSchemas,
111 $droppedSchemas,
112 $createdTables,
113 $alteredTables,
114 $droppedTables,
115 $createdSequences,
116 $alteredSequences,
117 $droppedSequences,
118 );
119 }
120
121 private function isAutoIncrementSequenceInSchema(Schema $schema, Sequence $sequence): bool
122 {
123 foreach ($schema->getTables() as $table) {
124 if ($sequence->isAutoIncrementsFor($table)) {
125 return true;
126 }
127 }
128
129 return false;
130 }
131
132 public function diffSequence(Sequence $sequence1, Sequence $sequence2): bool
133 {
134 if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) {
135 return true;
136 }
137
138 return $sequence1->getInitialValue() !== $sequence2->getInitialValue();
139 }
140
141 /**
142 * Compares the tables and returns the difference between them.
143 */
144 public function compareTables(Table $oldTable, Table $newTable): TableDiff
145 {
146 $addedColumns = [];
147 $modifiedColumns = [];
148 $droppedColumns = [];
149 $addedIndexes = [];
150 $modifiedIndexes = [];
151 $droppedIndexes = [];
152 $addedForeignKeys = [];
153 $modifiedForeignKeys = [];
154 $droppedForeignKeys = [];
155
156 $oldColumns = $oldTable->getColumns();
157 $newColumns = $newTable->getColumns();
158
159 // See if all the columns in the old table exist in the new table
160 foreach ($newColumns as $newColumn) {
161 $newColumnName = strtolower($newColumn->getName());
162
163 if ($oldTable->hasColumn($newColumnName)) {
164 continue;
165 }
166
167 $addedColumns[$newColumnName] = $newColumn;
168 }
169
170 // See if there are any removed columns in the new table
171 foreach ($oldColumns as $oldColumn) {
172 $oldColumnName = strtolower($oldColumn->getName());
173
174 // See if column is removed in the new table.
175 if (! $newTable->hasColumn($oldColumnName)) {
176 $droppedColumns[$oldColumnName] = $oldColumn;
177
178 continue;
179 }
180
181 $newColumn = $newTable->getColumn($oldColumnName);
182
183 if ($this->columnsEqual($oldColumn, $newColumn)) {
184 continue;
185 }
186
187 $modifiedColumns[] = new ColumnDiff($oldColumn, $newColumn);
188 }
189
190 $renamedColumns = $this->detectRenamedColumns($addedColumns, $droppedColumns);
191
192 $oldIndexes = $oldTable->getIndexes();
193 $newIndexes = $newTable->getIndexes();
194
195 // See if all the indexes from the old table exist in the new one
196 foreach ($newIndexes as $newIndexName => $newIndex) {
197 if (($newIndex->isPrimary() && $oldTable->getPrimaryKey() !== null) || $oldTable->hasIndex($newIndexName)) {
198 continue;
199 }
200
201 $addedIndexes[$newIndexName] = $newIndex;
202 }
203
204 // See if there are any removed indexes in the new table
205 foreach ($oldIndexes as $oldIndexName => $oldIndex) {
206 // See if the index is removed in the new table.
207 if (
208 ($oldIndex->isPrimary() && $newTable->getPrimaryKey() === null) ||
209 ! $oldIndex->isPrimary() && ! $newTable->hasIndex($oldIndexName)
210 ) {
211 $droppedIndexes[$oldIndexName] = $oldIndex;
212
213 continue;
214 }
215
216 // See if index has changed in the new table.
217 $newIndex = $oldIndex->isPrimary() ? $newTable->getPrimaryKey() : $newTable->getIndex($oldIndexName);
218 assert($newIndex instanceof Index);
219
220 if (! $this->diffIndex($oldIndex, $newIndex)) {
221 continue;
222 }
223
224 $modifiedIndexes[] = $newIndex;
225 }
226
227 $renamedIndexes = $this->detectRenamedIndexes($addedIndexes, $droppedIndexes);
228
229 $oldForeignKeys = $oldTable->getForeignKeys();
230 $newForeignKeys = $newTable->getForeignKeys();
231
232 foreach ($oldForeignKeys as $oldKey => $oldForeignKey) {
233 foreach ($newForeignKeys as $newKey => $newForeignKey) {
234 if ($this->diffForeignKey($oldForeignKey, $newForeignKey) === false) {
235 unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]);
236 } else {
237 if (strtolower($oldForeignKey->getName()) === strtolower($newForeignKey->getName())) {
238 $modifiedForeignKeys[] = $newForeignKey;
239
240 unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]);
241 }
242 }
243 }
244 }
245
246 foreach ($oldForeignKeys as $oldForeignKey) {
247 $droppedForeignKeys[] = $oldForeignKey;
248 }
249
250 foreach ($newForeignKeys as $newForeignKey) {
251 $addedForeignKeys[] = $newForeignKey;
252 }
253
254 return new TableDiff(
255 $oldTable,
256 $addedColumns,
257 $modifiedColumns,
258 $droppedColumns,
259 $renamedColumns,
260 $addedIndexes,
261 $modifiedIndexes,
262 $droppedIndexes,
263 $renamedIndexes,
264 $addedForeignKeys,
265 $modifiedForeignKeys,
266 $droppedForeignKeys,
267 );
268 }
269
270 /**
271 * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
272 * however ambiguities between different possibilities should not lead to renaming at all.
273 *
274 * @param array<string,Column> $addedColumns
275 * @param array<string,Column> $removedColumns
276 *
277 * @return array<string,Column>
278 */
279 private function detectRenamedColumns(array &$addedColumns, array &$removedColumns): array
280 {
281 $candidatesByName = [];
282
283 foreach ($addedColumns as $addedColumnName => $addedColumn) {
284 foreach ($removedColumns as $removedColumn) {
285 if (! $this->columnsEqual($addedColumn, $removedColumn)) {
286 continue;
287 }
288
289 $candidatesByName[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName];
290 }
291 }
292
293 $renamedColumns = [];
294
295 foreach ($candidatesByName as $candidates) {
296 if (count($candidates) !== 1) {
297 continue;
298 }
299
300 [$removedColumn, $addedColumn] = $candidates[0];
301 $removedColumnName = $removedColumn->getName();
302 $addedColumnName = strtolower($addedColumn->getName());
303
304 if (isset($renamedColumns[$removedColumnName])) {
305 continue;
306 }
307
308 $renamedColumns[$removedColumnName] = $addedColumn;
309 unset(
310 $addedColumns[$addedColumnName],
311 $removedColumns[strtolower($removedColumnName)],
312 );
313 }
314
315 return $renamedColumns;
316 }
317
318 /**
319 * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
320 * however ambiguities between different possibilities should not lead to renaming at all.
321 *
322 * @param array<string,Index> $addedIndexes
323 * @param array<string,Index> $removedIndexes
324 *
325 * @return array<string,Index>
326 */
327 private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndexes): array
328 {
329 $candidatesByName = [];
330
331 // Gather possible rename candidates by comparing each added and removed index based on semantics.
332 foreach ($addedIndexes as $addedIndexName => $addedIndex) {
333 foreach ($removedIndexes as $removedIndex) {
334 if ($this->diffIndex($addedIndex, $removedIndex)) {
335 continue;
336 }
337
338 $candidatesByName[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName];
339 }
340 }
341
342 $renamedIndexes = [];
343
344 foreach ($candidatesByName as $candidates) {
345 // If the current rename candidate contains exactly one semantically equal index,
346 // we can safely rename it.
347 // Otherwise, it is unclear if a rename action is really intended,
348 // therefore we let those ambiguous indexes be added/dropped.
349 if (count($candidates) !== 1) {
350 continue;
351 }
352
353 [$removedIndex, $addedIndex] = $candidates[0];
354
355 $removedIndexName = strtolower($removedIndex->getName());
356 $addedIndexName = strtolower($addedIndex->getName());
357
358 if (isset($renamedIndexes[$removedIndexName])) {
359 continue;
360 }
361
362 $renamedIndexes[$removedIndexName] = $addedIndex;
363 unset(
364 $addedIndexes[$addedIndexName],
365 $removedIndexes[$removedIndexName],
366 );
367 }
368
369 return $renamedIndexes;
370 }
371
372 protected function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2): bool
373 {
374 if (
375 array_map('strtolower', $key1->getUnquotedLocalColumns())
376 !== array_map('strtolower', $key2->getUnquotedLocalColumns())
377 ) {
378 return true;
379 }
380
381 if (
382 array_map('strtolower', $key1->getUnquotedForeignColumns())
383 !== array_map('strtolower', $key2->getUnquotedForeignColumns())
384 ) {
385 return true;
386 }
387
388 if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) {
389 return true;
390 }
391
392 if ($key1->onUpdate() !== $key2->onUpdate()) {
393 return true;
394 }
395
396 return $key1->onDelete() !== $key2->onDelete();
397 }
398
399 /**
400 * Compares the definitions of the given columns
401 */
402 protected function columnsEqual(Column $column1, Column $column2): bool
403 {
404 return $this->platform->columnsEqual($column1, $column2);
405 }
406
407 /**
408 * Finds the difference between the indexes $index1 and $index2.
409 *
410 * Compares $index1 with $index2 and returns true if there are any
411 * differences or false in case there are no differences.
412 */
413 protected function diffIndex(Index $index1, Index $index2): bool
414 {
415 return ! ($index1->isFulfilledBy($index2) && $index2->isFulfilledBy($index1));
416 }
417}