Completed
Pull Request — develop (#3570)
by Jonathan
155:39 queued 152:58
created

Comparator::compare()   F

Complexity

Conditions 25
Paths > 20000

Size

Total Lines 110
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 56
CRAP Score 25.0033

Importance

Changes 0
Metric Value
eloc 58
dl 0
loc 110
ccs 56
cts 57
cp 0.9825
rs 0
c 0
b 0
f 0
cc 25
nc 25200
nop 2
crap 25.0033

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Schema;
6
7
use Doctrine\DBAL\Types;
8
use function array_intersect_key;
9
use function array_key_exists;
10
use function array_keys;
11
use function array_map;
12
use function array_merge;
13
use function array_unique;
14
use function assert;
15
use function count;
16
use function strtolower;
17
18
/**
19
 * Compares two Schemas and return an instance of SchemaDiff.
20
 */
21
class Comparator
22
{
23 1311
    public static function compareSchemas(Schema $fromSchema, Schema $toSchema) : SchemaDiff
24
    {
25 1311
        $c = new self();
26
27 1311
        return $c->compare($fromSchema, $toSchema);
28
    }
29
30
    /**
31
     * Returns a SchemaDiff object containing the differences between the schemas $fromSchema and $toSchema.
32
     *
33
     * The returned differences are returned in such a way that they contain the
34
     * operations to change the schema stored in $fromSchema to the schema that is
35
     * stored in $toSchema.
36
     */
37 2503
    public function compare(Schema $fromSchema, Schema $toSchema) : SchemaDiff
38
    {
39 2503
        $diff             = new SchemaDiff();
40 2503
        $diff->fromSchema = $fromSchema;
41
42 2503
        $foreignKeysToTable = [];
43
44 2503
        foreach ($toSchema->getNamespaces() as $namespace) {
45 483
            if ($fromSchema->hasNamespace($namespace)) {
46 483
                continue;
47
            }
48
49 483
            $diff->newNamespaces[$namespace] = $namespace;
50
        }
51
52 2503
        foreach ($fromSchema->getNamespaces() as $namespace) {
53 483
            if ($toSchema->hasNamespace($namespace)) {
54 483
                continue;
55
            }
56
57 230
            $diff->removedNamespaces[$namespace] = $namespace;
58
        }
59
60 2503
        foreach ($toSchema->getTables() as $table) {
61 2503
            $tableName = $table->getShortestName($toSchema->getName());
62 2503
            if (! $fromSchema->hasTable($tableName)) {
63 1242
                $diff->newTables[$tableName] = $toSchema->getTable($tableName);
64
            } else {
65 2503
                $tableDifferences = $this->diffTable($fromSchema->getTable($tableName), $toSchema->getTable($tableName));
66 2503
                if ($tableDifferences !== null) {
67 2458
                    $diff->changedTables[$tableName] = $tableDifferences;
68
                }
69
            }
70
        }
71
72
        /* Check if there are tables removed */
73 2503
        foreach ($fromSchema->getTables() as $table) {
74 2503
            $tableName = $table->getShortestName($fromSchema->getName());
75
76 2503
            $table = $fromSchema->getTable($tableName);
77 2503
            if (! $toSchema->hasTable($tableName)) {
78 1265
                $diff->removedTables[$tableName] = $table;
79
            }
80
81
            // also remember all foreign keys that point to a specific table
82 2503
            foreach ($table->getForeignKeys() as $foreignKey) {
83 368
                $foreignTable = strtolower($foreignKey->getForeignTableName());
84 368
                if (! isset($foreignKeysToTable[$foreignTable])) {
85 368
                    $foreignKeysToTable[$foreignTable] = [];
86
                }
87 368
                $foreignKeysToTable[$foreignTable][] = $foreignKey;
88
            }
89
        }
90
91 2503
        foreach ($diff->removedTables as $tableName => $table) {
92 1265
            if (! isset($foreignKeysToTable[$tableName])) {
93 1265
                continue;
94
            }
95
96 368
            $diff->orphanedForeignKeys = array_merge($diff->orphanedForeignKeys, $foreignKeysToTable[$tableName]);
97
98
            // deleting duplicated foreign keys present on both on the orphanedForeignKey
99
            // and the removedForeignKeys from changedTables
100 368
            foreach ($foreignKeysToTable[$tableName] as $foreignKey) {
101
                // strtolower the table name to make if compatible with getShortestName
102 368
                $localTableName = strtolower($foreignKey->getLocalTableName());
103 368
                if (! isset($diff->changedTables[$localTableName])) {
104
                    continue;
105
                }
106
107 368
                foreach ($diff->changedTables[$localTableName]->removedForeignKeys as $key => $removedForeignKey) {
108 368
                    assert($removedForeignKey instanceof ForeignKeyConstraint);
109
110
                    // We check if the key is from the removed table if not we skip.
111 368
                    if ($tableName !== strtolower($removedForeignKey->getForeignTableName())) {
112 368
                        continue;
113
                    }
114 368
                    unset($diff->changedTables[$localTableName]->removedForeignKeys[$key]);
115
                }
116
            }
117
        }
118
119 2503
        foreach ($toSchema->getSequences() as $sequence) {
120 943
            $sequenceName = $sequence->getShortestName($toSchema->getName());
121 943
            if (! $fromSchema->hasSequence($sequenceName)) {
122 943
                if (! $this->isAutoIncrementSequenceInSchema($fromSchema, $sequence)) {
123 943
                    $diff->newSequences[] = $sequence;
124
                }
125
            } else {
126 805
                if ($this->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) {
127 552
                    $diff->changedSequences[] = $toSchema->getSequence($sequenceName);
128
                }
129
            }
130
        }
131
132 2503
        foreach ($fromSchema->getSequences() as $sequence) {
133 966
            if ($this->isAutoIncrementSequenceInSchema($toSchema, $sequence)) {
134 414
                continue;
135
            }
136
137 966
            $sequenceName = $sequence->getShortestName($fromSchema->getName());
138
139 966
            if ($toSchema->hasSequence($sequenceName)) {
140 805
                continue;
141
            }
142
143 966
            $diff->removedSequences[] = $sequence;
144
        }
145
146 2503
        return $diff;
147
    }
148
149 966
    private function isAutoIncrementSequenceInSchema(Schema $schema, Sequence $sequence) : bool
150
    {
151 966
        foreach ($schema->getTables() as $table) {
152 414
            if ($sequence->isAutoIncrementsFor($table)) {
153 414
                return true;
154
            }
155
        }
156
157 966
        return false;
158
    }
159
160 1595
    public function diffSequence(Sequence $sequence1, Sequence $sequence2) : bool
161
    {
162 1595
        if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) {
163 989
            return true;
164
        }
165
166 1595
        return $sequence1->getInitialValue() !== $sequence2->getInitialValue();
167
    }
168
169
    /**
170
     * Returns the difference between the tables $table1 and $table2.
171
     *
172
     * If there are no differences this method returns the boolean false.
173
     */
174 3229
    public function diffTable(Table $table1, Table $table2) : ?TableDiff
175
    {
176 3229
        $changes                     = 0;
177 3229
        $tableDifferences            = new TableDiff($table1->getName());
178 3229
        $tableDifferences->fromTable = $table1;
179
180 3229
        $table1Columns = $table1->getColumns();
181 3229
        $table2Columns = $table2->getColumns();
182
183
        /* See if all the fields in table 1 exist in table 2 */
184 3229
        foreach ($table2Columns as $columnName => $column) {
185 3229
            if ($table1->hasColumn($columnName)) {
186 3229
                continue;
187
            }
188
189 3160
            $tableDifferences->addedColumns[$columnName] = $column;
190 3160
            $changes++;
191
        }
192
        /* See if there are any removed fields in table 2 */
193 3229
        foreach ($table1Columns as $columnName => $column) {
194
            // See if column is removed in table 2.
195 3229
            if (! $table2->hasColumn($columnName)) {
196 2978
                $tableDifferences->removedColumns[$columnName] = $column;
197 2978
                $changes++;
198 2978
                continue;
199
            }
200
201
            // See if column has changed properties in table 2.
202 3229
            $changedProperties = $this->diffColumn($column, $table2->getColumn($columnName));
203
204 3229
            if (empty($changedProperties)) {
205 3229
                continue;
206
            }
207
208 3104
            $columnDiff                                           = new ColumnDiff($column->getName(), $table2->getColumn($columnName), $changedProperties);
209 3104
            $columnDiff->fromColumn                               = $column;
210 3104
            $tableDifferences->changedColumns[$column->getName()] = $columnDiff;
211 3104
            $changes++;
212
        }
213
214 3229
        $this->detectColumnRenamings($tableDifferences);
215
216 3229
        $table1Indexes = $table1->getIndexes();
217 3229
        $table2Indexes = $table2->getIndexes();
218
219
        /* See if all the indexes in table 1 exist in table 2 */
220 3229
        foreach ($table2Indexes as $indexName => $index) {
221 3193
            if (($index->isPrimary() && $table1->hasPrimaryKey()) || $table1->hasIndex($indexName)) {
222 3193
                continue;
223
            }
224
225 3109
            $tableDifferences->addedIndexes[$indexName] = $index;
226 3109
            $changes++;
227
        }
228
        /* See if there are any removed indexes in table 2 */
229 3229
        foreach ($table1Indexes as $indexName => $index) {
230
            // See if index is removed in table 2.
231 3193
            if (($index->isPrimary() && ! $table2->hasPrimaryKey()) ||
232 3193
                ! $index->isPrimary() && ! $table2->hasIndex($indexName)
233
            ) {
234 3081
                $tableDifferences->removedIndexes[$indexName] = $index;
235 3081
                $changes++;
236 3081
                continue;
237
            }
238
239
            // See if index has changed in table 2.
240 3193
            $table2Index = $index->isPrimary() ? $table2->getPrimaryKey() : $table2->getIndex($indexName);
241 3193
            assert($table2Index instanceof Index);
242
243 3193
            if (! $this->diffIndex($index, $table2Index)) {
244 3193
                continue;
245
            }
246
247 3160
            $tableDifferences->changedIndexes[$indexName] = $table2Index;
248 3160
            $changes++;
249
        }
250
251 3229
        $this->detectIndexRenamings($tableDifferences);
252
253 3229
        $fromFkeys = $table1->getForeignKeys();
254 3229
        $toFkeys   = $table2->getForeignKeys();
255
256 3229
        foreach ($fromFkeys as $key1 => $constraint1) {
257 3008
            foreach ($toFkeys as $key2 => $constraint2) {
258 3008
                if ($this->diffForeignKey($constraint1, $constraint2) === false) {
259 2991
                    unset($fromFkeys[$key1], $toFkeys[$key2]);
260
                } else {
261 2978
                    if (strtolower($constraint1->getName()) === strtolower($constraint2->getName())) {
262 874
                        $tableDifferences->changedForeignKeys[] = $constraint2;
263 874
                        $changes++;
264 874
                        unset($fromFkeys[$key1], $toFkeys[$key2]);
265
                    }
266
                }
267
            }
268
        }
269
270 3229
        foreach ($fromFkeys as $constraint1) {
271 2978
            $tableDifferences->removedForeignKeys[] = $constraint1;
272 2978
            $changes++;
273
        }
274
275 3229
        foreach ($toFkeys as $constraint2) {
276 2978
            $tableDifferences->addedForeignKeys[] = $constraint2;
277 2978
            $changes++;
278
        }
279
280 3229
        return $changes ? $tableDifferences : null;
281
    }
282
283
    /**
284
     * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
285
     * however ambiguities between different possibilities should not lead to renaming at all.
286
     */
287 3229
    private function detectColumnRenamings(TableDiff $tableDifferences) : void
288
    {
289 3229
        $renameCandidates = [];
290 3229
        foreach ($tableDifferences->addedColumns as $addedColumnName => $addedColumn) {
291 3160
            foreach ($tableDifferences->removedColumns as $removedColumn) {
292 2978
                if (count($this->diffColumn($addedColumn, $removedColumn)) !== 0) {
293 2691
                    continue;
294
                }
295
296 2978
                $renameCandidates[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName];
297
            }
298
        }
299
300 3229
        foreach ($renameCandidates as $candidateColumns) {
301 2978
            if (count($candidateColumns) !== 1) {
302 644
                continue;
303
            }
304
305 2978
            [$removedColumn, $addedColumn] = $candidateColumns[0];
306 2978
            $removedColumnName             = strtolower($removedColumn->getName());
307 2978
            $addedColumnName               = strtolower($addedColumn->getName());
308
309 2978
            if (isset($tableDifferences->renamedColumns[$removedColumnName])) {
310 1104
                continue;
311
            }
312
313 2978
            $tableDifferences->renamedColumns[$removedColumnName] = $addedColumn;
314
            unset(
315 2978
                $tableDifferences->addedColumns[$addedColumnName],
316 2978
                $tableDifferences->removedColumns[$removedColumnName]
317
            );
318
        }
319 3229
    }
320
321
    /**
322
     * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
323
     * however ambiguities between different possibilities should not lead to renaming at all.
324
     */
325 3229
    private function detectIndexRenamings(TableDiff $tableDifferences) : void
326
    {
327 3229
        $renameCandidates = [];
328
329
        // Gather possible rename candidates by comparing each added and removed index based on semantics.
330 3229
        foreach ($tableDifferences->addedIndexes as $addedIndexName => $addedIndex) {
331 3109
            foreach ($tableDifferences->removedIndexes as $removedIndex) {
332 3081
                if ($this->diffIndex($addedIndex, $removedIndex)) {
333 2955
                    continue;
334
                }
335
336 2955
                $renameCandidates[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName];
337
            }
338
        }
339
340 3229
        foreach ($renameCandidates as $candidateIndexes) {
341
            // If the current rename candidate contains exactly one semantically equal index,
342
            // we can safely rename it.
343
            // Otherwise it is unclear if a rename action is really intended,
344
            // therefore we let those ambiguous indexes be added/dropped.
345 2955
            if (count($candidateIndexes) !== 1) {
346 598
                continue;
347
            }
348
349 2955
            [$removedIndex, $addedIndex] = $candidateIndexes[0];
350
351 2955
            $removedIndexName = strtolower($removedIndex->getName());
352 2955
            $addedIndexName   = strtolower($addedIndex->getName());
353
354 2955
            if (isset($tableDifferences->renamedIndexes[$removedIndexName])) {
355
                continue;
356
            }
357
358 2955
            $tableDifferences->renamedIndexes[$removedIndexName] = $addedIndex;
359
            unset(
360 2955
                $tableDifferences->addedIndexes[$addedIndexName],
361 2955
                $tableDifferences->removedIndexes[$removedIndexName]
362
            );
363
        }
364 3229
    }
365
366 3008
    public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2) : bool
367
    {
368 3008
        if (array_map('strtolower', $key1->getUnquotedLocalColumns()) !== array_map('strtolower', $key2->getUnquotedLocalColumns())) {
369 2978
            return true;
370
        }
371
372 2991
        if (array_map('strtolower', $key1->getUnquotedForeignColumns()) !== array_map('strtolower', $key2->getUnquotedForeignColumns())) {
373
            return true;
374
        }
375
376 2991
        if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) {
377 851
            return true;
378
        }
379
380 2991
        if ($key1->onUpdate() !== $key2->onUpdate()) {
381 874
            return true;
382
        }
383
384 2991
        return $key1->onDelete() !== $key2->onDelete();
385
    }
386
387
    /**
388
     * Returns the difference between the fields $field1 and $field2.
389
     *
390
     * If there are differences this method returns $field2, otherwise the
391
     * boolean false.
392
     *
393
     * @return array<int, string>
394
     */
395 3229
    public function diffColumn(Column $column1, Column $column2) : array
396
    {
397 3229
        $properties1 = $column1->toArray();
398 3229
        $properties2 = $column2->toArray();
399
400 3229
        $changedProperties = [];
401
402 3229
        foreach (['type', 'notnull', 'unsigned', 'autoincrement'] as $property) {
403 3229
            if ($properties1[$property] === $properties2[$property]) {
404 3229
                continue;
405
            }
406
407 2193
            $changedProperties[] = $property;
408
        }
409
410
        // Null values need to be checked additionally as they tell whether to create or drop a default value.
411
        // null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation.
412 3229
        if (($properties1['default'] === null) !== ($properties2['default'] === null)
413 3229
            || $properties1['default'] != $properties2['default']) {
414 3012
            $changedProperties[] = 'default';
415
        }
416
417 3229
        if (($properties1['type'] instanceof Types\StringType && ! $properties1['type'] instanceof Types\GuidType) ||
418 3229
            $properties1['type'] instanceof Types\BinaryType
419
        ) {
420
            // check if value of length is set at all, default value assumed otherwise.
421 3193
            $length1 = $properties1['length'] ?: 255;
422 3193
            $length2 = $properties2['length'] ?: 255;
423 3193
            if ($length1 !== $length2) {
424 2461
                $changedProperties[] = 'length';
425
            }
426
427 3193
            if ($properties1['fixed'] !== $properties2['fixed']) {
428 3193
                $changedProperties[] = 'fixed';
429
            }
430 3229
        } elseif ($properties1['type'] instanceof Types\DecimalType) {
431 3001
            if (($properties1['precision'] ?: 10) !== ($properties2['precision'] ?: 10)) {
432
                $changedProperties[] = 'precision';
433
            }
434 3001
            if ($properties1['scale'] !== $properties2['scale']) {
435
                $changedProperties[] = 'scale';
436
            }
437
        }
438
439
        // A null value and an empty string are actually equal for a comment so they should not trigger a change.
440 3229
        if ($properties1['comment'] !== $properties2['comment'] &&
441 3229
            ! ($properties1['comment'] === null && $properties2['comment'] === '') &&
442 3229
            ! ($properties2['comment'] === null && $properties1['comment'] === '')
443
        ) {
444 2909
            $changedProperties[] = 'comment';
445
        }
446
447 3229
        $customOptions1 = $column1->getCustomSchemaOptions();
448 3229
        $customOptions2 = $column2->getCustomSchemaOptions();
449
450 3229
        foreach (array_merge(array_keys($customOptions1), array_keys($customOptions2)) as $key) {
451 1127
            if (! array_key_exists($key, $properties1) || ! array_key_exists($key, $properties2)) {
452 1127
                $changedProperties[] = $key;
453 1127
            } elseif ($properties1[$key] !== $properties2[$key]) {
454
                $changedProperties[] = $key;
455
            }
456
        }
457
458 3229
        $platformOptions1 = $column1->getPlatformOptions();
459 3229
        $platformOptions2 = $column2->getPlatformOptions();
460
461 3229
        foreach (array_keys(array_intersect_key($platformOptions1, $platformOptions2)) as $key) {
462 2098
            if ($properties1[$key] === $properties2[$key]) {
463 2098
                continue;
464
            }
465
466 1990
            $changedProperties[] = $key;
467
        }
468
469 3229
        return array_unique($changedProperties);
470
    }
471
472
    /**
473
     * Finds the difference between the indexes $index1 and $index2.
474
     *
475
     * Compares $index1 with $index2 and returns $index2 if there are any
476
     * differences or false in case there are no differences.
477
     */
478 3193
    public function diffIndex(Index $index1, Index $index2) : bool
479
    {
480 3193
        return ! ($index1->isFullfilledBy($index2) && $index2->isFullfilledBy($index1));
481
    }
482
}
483