GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( b4c702...a89058 )
by
unknown
08:51 queued 24s
created

CreateTable::diffColumns()   C

Complexity

Conditions 13
Paths 87

Size

Total Lines 75
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 13

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 39
nc 87
nop 1
dl 0
loc 75
ccs 35
cts 35
cp 1
crap 13
rs 6.6166
c 1
b 0
f 0

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
namespace Graze\Morphism\Parse;
3
4
use RuntimeException;
5
6
/**
7
 * Represents a table definition.
8
 */
9
class CreateTable
10
{
11
    /** @var string */
12
    private $name = '';
13
14
    /** @var ColumnDefinition[] */
15
    public $columns = [];
16
17
    /** @var IndexDefinition[] definitions of non-foreign keys */
18
    public $indexes = [];
19
20
    /** @var IndexDefinition[] definitions of foreign keys */
21
    public $foreigns = [];
22
23
    /** @var CheckDefinition[] definitions of checks */
24
    public $checks = [];
25
26
    /** @var TableOptions */
27
    public $options = null;
28
29
    /** @var array */
30
    private $covers = [];
31
    
32
    /** @var bool */
33
    private $addIndexForForeignKey = true;
34
35
    /**
36
     * Constructor.
37
     *
38
     * @param CollationInfo $databaseCollation
39
     */
40 100
    public function __construct(CollationInfo $databaseCollation)
41
    {
42 100
        $this->options = new TableOptions($databaseCollation);
43 100
    }
44
45
    /**
46
     * @return string
47
     */
48 1
    public function getName()
49
    {
50 1
        return $this->name;
51
    }
52
53
    /**
54
     * Sets the storage engine the table is assumed to use, unless
55
     * explicitly overridden via an ENGINE= clause at the end of
56
     * the table definition.
57
     *
58
     * @param string $engine
59
     * @return void
60
     */
61 98
    public function setDefaultEngine($engine)
62
    {
63 98
        $this->options->setDefaultEngine($engine);
64 98
    }
65
66
    /**
67
     * Sets whether to add an index for each foreign key if one isn't defined, this is the default behaviour of MySQL.
68
     *
69
     * @param bool $addIndexForForeignKey
70
     */
71 24
    public function setAddIndexForForeignKey($addIndexForForeignKey)
72
    {
73 24
        $this->addIndexForForeignKey = $addIndexForForeignKey;
74 24
    }
75
76
    /**
77
     * Parses a table definition from $stream.
78
     *
79
     * The DDL may be of the form 'CREATE TABLE ...' or 'CREATE TABLE IF NOT EXISTS ...'.
80
     *
81
     * An exception will be thrown if a valid CREATE TABLE statement cannot be recognised.
82
     *
83
     * @param TokenStream $stream
84
     */
85 97
    public function parse(TokenStream $stream)
86
    {
87 97
        if ($stream->consume('CREATE TABLE')) {
88 96
            $stream->consume('IF NOT EXISTS');
89
        } else {
90 1
            throw new RuntimeException("Expected CREATE TABLE");
91
        }
92
93 96
        $this->name = $stream->expectName();
94 96
        $stream->expectOpenParen();
95
96 96
        while (true) {
97 96
            $hasConstraintKeyword = $stream->consume('CONSTRAINT');
98 96
            if ($stream->consume('PRIMARY KEY')) {
99 10
                $this->parseIndex($stream, 'PRIMARY KEY');
100 96
            } elseif ($stream->consume('KEY') ||
101 96
                $stream->consume('INDEX')
102
            ) {
103 19
                if ($hasConstraintKeyword) {
104 2
                    throw new RuntimeException("Bad CONSTRAINT");
105
                }
106 17
                $this->parseIndex($stream, 'KEY');
107 96
            } elseif ($stream->consume('FULLTEXT')) {
108 4
                if ($hasConstraintKeyword) {
109 1
                    throw new RuntimeException("Bad CONSTRAINT");
110
                }
111 3
                $stream->consume('KEY') || $stream->consume('INDEX');
112 3
                $this->parseIndex($stream, 'FULLTEXT KEY');
113 96
            } elseif ($stream->consume('UNIQUE')) {
114 5
                $stream->consume('KEY') || $stream->consume('INDEX');
115 5
                $this->parseIndex($stream, 'UNIQUE KEY');
116 96
            } elseif ($stream->consume('FOREIGN KEY')) {
117 10
                $this->parseIndex($stream, 'FOREIGN KEY');
118 96
            } elseif ($hasConstraintKeyword) {
119 9
                $constraint = $stream->expectName();
120 9
                if ($stream->consume('PRIMARY KEY')) {
121 1
                    $this->parseIndex($stream, 'PRIMARY KEY', $constraint);
122 8
                } elseif ($stream->consume('UNIQUE')) {
123 1
                    $stream->consume('KEY') || $stream->consume('INDEX');
124 1
                    $this->parseIndex($stream, 'UNIQUE KEY', $constraint);
125 7
                } elseif ($stream->consume('FOREIGN KEY')) {
126 6
                    $this->parseIndex($stream, 'FOREIGN KEY', $constraint);
127 1
                } elseif ($stream->consume('CHECK')) {
128
                    $this->parseCheck($stream, $constraint);
129
                } else {
130 9
                    throw new RuntimeException("Bad CONSTRAINT");
131
                }
132
            } else {
133 96
                $this->parseColumn($stream);
134
            }
135 93
            $token = $stream->nextToken();
136 93
            if ($token->eq(Token::SYMBOL, ',')) {
137 68
                continue;
138 87
            } elseif ($token->eq(Token::SYMBOL, ')')) {
139 86
                break;
140
            } else {
141 1
                throw new RuntimeException("Expected ',' or ')'");
142
            }
143
        }
144
145 86
        $this->processTimestamps();
146 86
        $this->processIndexes();
147 82
        $this->processAutoIncrement();
148 80
        $this->parseTableOptions($stream);
149 80
        $this->processColumnCollations();
150 80
    }
151
152
    /**
153
     * Returns the table's collation.
154
     *
155
     * @return CollationInfo
156
     */
157 80
    public function getCollation()
158
    {
159 80
        return $this->options->collation;
160
    }
161
162
    /**
163
     * Returns an array of SQL DDL statements to create the table.
164
     *
165
     * @return array
166
     */
167 53
    public function getDDL()
168
    {
169 53
        $lines = [];
170 53
        foreach ($this->columns as $column) {
171 53
            $lines[] = "  " . $column->toString($this->getCollation());
172
        }
173 53
        foreach ($this->indexes as $index) {
174 35
            $lines[] = "  " . $index->toString();
175
        }
176 53
        foreach ($this->foreigns as $foreign) {
177 9
            $lines[] = "  " . $foreign->toString();
178
        }
179
180 53
        $text = "CREATE TABLE " . Token::escapeIdentifier($this->name) . " (\n" .
181 53
            implode(",\n", $lines) .
182 53
            "\n" .
183 53
            ")";
184
185 53
        $options = $this->options->toString();
186 53
        if ($options !== '') {
187 53
            $text .= " " . $this->options->toString();
188
        }
189
190 53
        return [$text];
191
    }
192
193
    /**
194
     * @param TokenStream $stream
195
     */
196 96
    private function parseColumn(TokenStream $stream)
197
    {
198 96
        $column = new ColumnDefinition();
199 96
        $column->parse($stream);
200 93
        if (array_key_exists(strtolower($column->name), $this->columns)) {
201 2
            throw new RuntimeException("Duplicate column name '" . $column->name . "'");
202
        }
203 93
        $this->columns[strtolower($column->name)] = $column;
204 93
        $this->indexes = array_merge(
205 93
            $this->indexes,
206 93
            $column->indexes
207
        );
208 93
    }
209
210
    /**
211
     * @param TokenStream $stream
212
     * @param string $type
213
     * @param string|null $constraint
214
     */
215 45
    private function parseIndex(TokenStream $stream, $type, $constraint = null)
216
    {
217 45
        $index = new IndexDefinition();
218 45
        $index->parse($stream, $type, $constraint);
219 45
        $this->indexes[] = $index;
220 45
    }
221
222
    /**
223
     * @param TokenStream $stream
224
     */
225 80
    private function parseTableOptions(TokenStream $stream)
226
    {
227 80
        $this->options->parse($stream);
228 80
    }
229
230 86
    private function processTimestamps()
231
    {
232
        // To specify automatic properties, use the DEFAULT CURRENT_TIMESTAMP
233
        // and ON UPDATE CURRENT_TIMESTAMP clauses. The order of the clauses
234
        // does not matter. If both are present in a column definition, either
235
        // can occur first.
236
237
        // collect all timestamps
238 86
        $ts = [];
239 86
        foreach ($this->columns as $column) {
240 86
            if ($column->type === 'timestamp') {
241 12
                $ts[] = $column;
242
            }
243
        }
244 86
        if (count($ts) === 0) {
245 74
            return;
246
        }
247
248
        // none of NULL, DEFAULT or ON UPDATE CURRENT_TIMESTAMP have been specified
249 12
        if (!$ts[0]->nullable && is_null($ts[0]->default) && !$ts[0]->onUpdateCurrentTimestamp) {
250 3
            $ts[0]->nullable = false;
251 3
            $ts[0]->default = 'CURRENT_TIMESTAMP';
252 3
            $ts[0]->onUpdateCurrentTimestamp = true;
253
        }
254
255
        // [[ this restriction no longer exists as of MySQL 5.6.5 and MariaDB 10.0.1 ]]
256
257
        // One TIMESTAMP column in a table can have the current timestamp as
258
        // the default value for initializing the column, as the auto-update
259
        // value, or both. It is not possible to have the current timestamp
260
        // be the default value for one column and the auto-update value for
261
        // another column.
262
263
        // $specials = 0;
264
        // foreach($ts as $column) {
265
        //     if ($column->default === 'CURRENT_TIMESTAMP' ||
266
        //         $column->onUpdateCurrentTimestamp
267
        //     ) {
268
        //         if (++$specials > 1) {
269
        //             throw new RuntimeException("There can be only one TIMESTAMP column with CURRENT_TIMESTAMP in DEFAULT or ON UPDATE clause");
270
        //         }
271
        //     }
272
        // }
273
274 12
        foreach ($ts as $column) {
275 12
            if (!$column->nullable && is_null($column->default)) {
276 4
                $column->default = '0000-00-00 00:00:00';
277
            }
278
        }
279 12
    }
280
281 86
    private function processIndexes()
282
    {
283
        // check indexes are sane wrt available columns
284 86
        foreach ($this->indexes as $index) {
285 53
            foreach ($index->columns as $indexColumn) {
286 53
                $indexColumnName = $indexColumn['name'];
287 53
                if (!array_key_exists(strtolower($indexColumnName), $this->columns)) {
288 1
                    throw new RuntimeException("Key column '$indexColumnName' doesn't exist in table");
289
                }
290
            }
291
        }
292
293
        // figure out all sequences of columns covered by non-FK indexes
294 85
        foreach ($this->indexes as $index) {
295 52
            if ($index->type !== 'FOREIGN KEY') {
296 42
                foreach ($index->getCovers() as $cover) {
297 42
                    $lookup = implode('\0', $cover);
298 42
                    $this->covers[$lookup] = true;
299
                }
300
            }
301
        }
302
303 85
        $indexes = [];
304 85
        $foreigns = [];
305 85
        $ibfkCounter = 0;
306
307 85
        foreach ($this->indexes as $index) {
308 52
            if ($index->type === 'FOREIGN KEY') {
309 16
                if ($this->addIndexForForeignKey) {
310
                    // TODO - doesn't correctly deal with indexes like foo(10)
311 14
                    $lookup = implode('\0', $index->getColumns());
312 14
                    if (!array_key_exists($lookup, $this->covers)) {
313 12
                        $newIndex = new IndexDefinition();
314 12
                        $newIndex->type = 'KEY';
315 12
                        $newIndex->columns = $index->columns;
316 12
                        if (!is_null($index->constraint)) {
317 5
                            $newIndex->name = $index->constraint;
318 7
                        } elseif (!is_null($index->name)) {
319 1
                            $newIndex->name = $index->name;
320
                        }
321 12
                        $indexes[] = $newIndex;
322
                    }
323
                }
324
325 16
                $foreign = new IndexDefinition();
326 16
                if (is_null($index->constraint)) {
327 10
                    $foreign->constraint = $this->name . '_ibfk_' . ++$ibfkCounter;
328
                } else {
329 6
                    $foreign->constraint = $index->constraint;
330
                }
331 16
                $foreign->type = 'FOREIGN KEY';
332 16
                $foreign->columns = $index->columns;
333 16
                $foreign->reference = $index->reference;
334 16
                $foreigns[] = $foreign;
335
            } else {
336 42
                $indexes[] = $index;
337
            }
338
        }
339
340
        // now synthesise names for any unnamed indexes,
341
        // and collect indexes by type
342 85
        $usedName = [];
343
        $keyTypes = [
344 85
            'PRIMARY KEY',
345
            'UNIQUE KEY',
346
            'KEY',
347
            'FULLTEXT KEY',
348
            'FOREIGN KEY',
349
        ];
350 85
        $indexesByType = array_fill_keys($keyTypes, []);
351 85
        foreach ($indexes as $index) {
352 50
            $name = $index->name;
353 50
            if ($index->type === 'PRIMARY KEY') {
354 19
                $name = 'PRIMARY';
355 32
            } elseif (is_null($name)) {
356 17
                $base = $index->columns[0]['name'];
357 17
                $name = $base;
358 17
                $i = 1;
359 17
                while (isset($usedName[$name])) {
360 1
                    $name = $base . '_' . ++$i;
361
                }
362 17
                $index->name = $name;
363 18
            } elseif (array_key_exists(strtolower($name), $usedName)) {
364 2
                throw new RuntimeException("Duplicate key name '$name'");
365
            }
366 50
            $index->name = $name;
367 50
            $usedName[strtolower($name)] = true;
368
369 50
            $indexesByType[$index->type][] = $index;
370
        }
371
372 83
        if (count($indexesByType['PRIMARY KEY']) > 1) {
373 1
            throw new RuntimeException("Multiple PRIMARY KEYs defined");
374
        }
375
376 82
        foreach ($indexesByType['PRIMARY KEY'] as $pk) {
377 18
            foreach ($pk->columns as $indexColumn) {
378 18
                $column = $this->columns[strtolower($indexColumn['name'])];
379 18
                if ($column->nullable) {
380 10
                    $column->nullable = false;
381 10
                    if (is_null($column->default)) {
382 10
                        $column->default = $column->getUninitialisedValue();
383
                    }
384
                }
385
            }
386
        }
387
388 82
        $this->indexes = [];
389 82
        foreach (array_reduce($indexesByType, 'array_merge', []) as $index) {
390 47
            $this->indexes[$index->name] = $index;
391
        }
392 82
        foreach ($foreigns as $foreign) {
393 16
            $this->foreigns[$foreign->constraint] = $foreign;
394
        }
395 82
    }
396
397 82
    private function processAutoIncrement()
398
    {
399 82
        $count = 0;
400 82
        foreach ($this->columns as $column) {
401 82
            if ($column->autoIncrement) {
402 3
                if (++$count > 1) {
403 1
                    throw new RuntimeException("There can be only one AUTO_INCREMENT column");
404
                }
405 3
                if (!array_key_exists($column->name, $this->covers)) {
406 1
                    throw new RuntimeException("AUTO_INCREMENT column must be defined as a key");
407
                }
408
            }
409
        }
410 80
    }
411
412 80
    private function processColumnCollations()
413
    {
414 80
        foreach ($this->columns as $column) {
415 80
            $column->applyTableCollation($this->getCollation());
416
        }
417 80
    }
418
419
    /**
420
     * Returns ALTER TABLE statement to transform this table into the one
421
     * represented by $that. If the tables are already equivalent, just
422
     * returns the empty string.
423
     *
424
     * $flags        |
425
     * :-------------|----
426
     * 'alterEngine' | (bool) include ALTER TABLE ... ENGINE= [default: true]
427
     *
428
     * @param CreateTable $that
429
     * @param array $flags
430
     * @return string[]
431
     */
432 27
    public function diff(CreateTable $that, array $flags = [])
433
    {
434
        $flags += [
435 27
            'alterEngine' => true
436
        ];
437
438 27
        $alters = array_merge(
439 27
            $this->diffColumns($that),
440 27
            $this->diffIndexes($that),
441 27
            $this->diffForeigns($that),
442 27
            $this->diffOptions($that, [
443 27
                'alterEngine' => $flags['alterEngine']
444
            ])
445
        );
446
447 27
        if (count($alters) === 0) {
448 3
            return [];
449
        }
450
451 24
        return ["ALTER TABLE " . Token::escapeIdentifier($this->name) . "\n" . implode(",\n", $alters)];
452
    }
453
454
    /**
455
     * @param CreateTable $that
456
     * @return array
457
     */
458 27
    private function diffColumns(CreateTable $that)
459
    {
460 27
        $alters = [];
461 27
        $permutation = [];
462 27
        foreach (array_keys($this->columns) as $columnName) {
463 27
            if (array_key_exists($columnName, $that->columns)) {
464 27
                $permutation[] = $columnName;
465
            } else {
466 1
                $alters[] = "DROP COLUMN " . Token::escapeIdentifier($columnName);
467
            }
468
        }
469
470 27
        $prevColumn = null;
471 27
        $thatPosition = " FIRST";
472 27
        $j = 0;
473 27
        foreach ($that->columns as $columnName => $column) {
474 27
            if (array_key_exists($columnName, $this->columns)) {
475
                // An existing column is being changed
476 27
                $thisDefinition = $this->columns[$columnName]->toString($this->getCollation());
477 27
                $thatDefinition = $that->columns[$columnName]->toString($that->getCollation());
478
                
479
                if (str_contains($thisDefinition, 'utf8mb3')) {
480
                    $thisDefinition = str_replace('utf8mb3', 'utf8', $thisDefinition);
481 27
                }
482
                if (str_contains($thatDefinition, 'utf8mb3')) {
483
                    $thatDefinition = str_replace('utf8mb3', 'utf8', $thatDefinition);
484
                }
485 27
486
                // about to 'add' $columnName - get its location in the currently
487 27
                // permuted state of the tabledef
488 27
                $i = array_search($columnName, $permutation);
489
490 8
                // figure out the column it currently sits after, in case we
491
                // need to change it
492
                $thisPosition = ($i === 0) ? " FIRST" : " AFTER " . Token::escapeIdentifier($permutation[$i - 1]);
493 8
494 3
                if ($thisDefinition !== $thatDefinition ||
495
                    $thisPosition   !== $thatPosition
496
                ) {
497
                    $alter = "MODIFY COLUMN " . $thatDefinition;
498
499
                    // position has changed
500 3
                    if ($thisPosition !== $thatPosition) {
501
                        $alter .= $thatPosition;
502
503 3
                        // We need to update our permutation to reflect the new position.
504
                        // Column is being inserted at position $j, and is currently residing at $i.
505
506 27
                        // remove from current location
507
                        array_splice($permutation, /** @scrutinizer ignore-type */ $i, 1, []);
508
509
                        // insert at new location
510 3
                        array_splice($permutation, $j, 0, $columnName);
511 3
                    }
512 2
513
                    $alters[] = $alter;
514 3
                }
515
            } else {
516 3
                // A new column is being added
517 3
                $alter = "ADD COLUMN " . $column->toString($this->getCollation());
518
                if ($j < count($permutation)) {
519
                    $alter .= $thatPosition;
520 27
                }
521 27
                $alters[] = $alter;
522 27
523
                $i = is_null($prevColumn) ? 0 : 1 + array_search($prevColumn, $permutation);
524
                array_splice($permutation, $i, 0, [$columnName]);
525 27
            }
526
527
            $prevColumn = $columnName;
528
            $thatPosition = " AFTER " . Token::escapeIdentifier($prevColumn);
529
            $j++;
530
        }
531
532 27
        return $alters;
533
    }
534 27
535
    /**
536 27
     * @param CreateTable $that
537 5
     * @return array
538 5
     */
539
    private function diffIndexes(CreateTable $that)
540 4
    {
541 4
        $alters = [];
542 1
543 1
        foreach ($this->indexes as $indexName => $index) {
544
            if (!array_key_exists($indexName, $that->indexes) ||
545
                $index->toString() !== $that->indexes[$indexName]->toString()
546 3
            ) {
547 3
                switch ($index->type) {
548
                    case 'PRIMARY KEY':
549 4
                        $alter = "DROP PRIMARY KEY";
550
                        break;
551
552
                    default:
553 27
                        $alter = "DROP KEY " . Token::escapeIdentifier($indexName);
554 9
                        break;
555 9
                }
556
                $alters[] = $alter;
557 8
            }
558
        }
559
560
        foreach ($that->indexes as $indexName => $index) {
561 27
            if (!array_key_exists($indexName, $this->indexes) ||
562
                $index->toString() !== $this->indexes[$indexName]->toString()
563
            ) {
564
                $alters[] = "ADD " . $index->toString();
565
            }
566
        }
567
568 27
        return $alters;
569
    }
570 27
571
    /**
572 27
     * @param CreateTable $that
573 5
     * @return array
574 5
     */
575
    private function diffForeigns(CreateTable $that)
576 3
    {
577
        $alters = [];
578
579
        foreach ($this->foreigns as $foreignName => $foreign) {
580 27
            if (!array_key_exists($foreignName, $that->foreigns) ||
581 5
                $foreign->toString() !== $that->foreigns[$foreignName]->toString()
582 5
            ) {
583
                $alters[] = "DROP FOREIGN KEY " . Token::escapeIdentifier($foreignName);
584 3
            }
585
        }
586
587
        foreach ($that->foreigns as $foreignName => $foreign) {
588 27
            if (!array_key_exists($foreignName, $this->foreigns) ||
589
                $foreign->toString() !== $this->foreigns[$foreignName]->toString()
590
            ) {
591
                $alters[] = "ADD " . $foreign->toString();
592
            }
593
        }
594
595
        return $alters;
596 27
    }
597
598
    /**
599 27
     * @param CreateTable $that
600
     * @param array $flags
601 27
     * @return array
602 27
     */
603
    private function diffOptions(CreateTable $that, array $flags = [])
604 27
    {
605
        $flags += [
606
            'alterEngine' => true
607
        ];
608
        $diff = $this->options->diff($that->options, [
609
            'alterEngine' => $flags['alterEngine']
610
        ]);
611
        return ($diff == '') ? [] : [$diff];
612
    }
613
614
    /**
615
     * @param TokenStream $stream
616
     * @param string|null $name
617
     */
618
    private function parseCheck(TokenStream $stream, $name = null)
619
    {
620
        $check = new CheckDefinition();
621
        $check->parse($stream, $name);
622
        $this->checks[] = $check;
623
    }
624
}
625