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
Pull Request — master (#49)
by Burhan
02:28
created

CreateTable::processIndexes()   F

Complexity

Conditions 25
Paths 10201

Size

Total Lines 110
Code Lines 74

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 68
CRAP Score 25

Importance

Changes 0
Metric Value
cc 25
eloc 74
nc 10201
nop 0
dl 0
loc 110
rs 0
c 0
b 0
f 0
ccs 68
cts 68
cp 1
crap 25

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
    public $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 TableOptions */
24
    public $options = null;
25
26
    /** @var array */
27
    private $covers = [];
28
29
    /**
30
     * Constructor.
31
     *
32
     * @param CollationInfo $databaseCollation
33
     */
34 92
    public function __construct(CollationInfo $databaseCollation)
35
    {
36 92
        $this->options = new TableOptions($databaseCollation);
37 92
    }
38
39
    /**
40
     * Sets the storage engine the table is assumed to use, unless
41
     * explicitly overridden via an ENGINE= clause at the end of
42
     * the table definition.
43
     *
44
     * @param string $engine
45
     * @return void
46
     */
47 91
    public function setDefaultEngine($engine)
48
    {
49 91
        $this->options->setDefaultEngine($engine);
50 91
    }
51
52
    /**
53
     * Parses a table definition from $stream.
54
     *
55
     * The DDL may be of the form 'CREATE TABLE ...' or 'CREATE TABLE IF NOT EXISTS ...'.
56
     *
57
     * An exception will be thrown if a valid CREATE TABLE statement cannot be recognised.
58
     *
59
     * @param TokenStream $stream
60
     */
61 90
    public function parse(TokenStream $stream)
62
    {
63 90
        if ($stream->consume('CREATE TABLE')) {
64 89
            $stream->consume('IF NOT EXISTS');
65
        } else {
66 1
            throw new RuntimeException("Expected CREATE TABLE");
67
        }
68
69 89
        $this->name = $stream->expectName();
70 89
        $stream->expectOpenParen();
71
72 89
        while (true) {
73 89
            $hasConstraintKeyword = $stream->consume('CONSTRAINT');
74 89
            if ($stream->consume('PRIMARY KEY')) {
75 10
                $this->parseIndex($stream, 'PRIMARY KEY');
76 89
            } elseif ($stream->consume('KEY') ||
77 89
                $stream->consume('INDEX')
78
            ) {
79 18
                if ($hasConstraintKeyword) {
80 2
                    throw new RuntimeException("Bad CONSTRAINT");
81
                }
82 16
                $this->parseIndex($stream, 'KEY');
83 89
            } elseif ($stream->consume('FULLTEXT')) {
84 4
                if ($hasConstraintKeyword) {
85 1
                    throw new RuntimeException("Bad CONSTRAINT");
86
                }
87 3
                $stream->consume('KEY') || $stream->consume('INDEX');
88 3
                $this->parseIndex($stream, 'FULLTEXT KEY');
89 89
            } elseif ($stream->consume('UNIQUE')) {
90 5
                $stream->consume('KEY') || $stream->consume('INDEX');
91 5
                $this->parseIndex($stream, 'UNIQUE KEY');
92 89
            } elseif ($stream->consume('FOREIGN KEY')) {
93 8
                $this->parseIndex($stream, 'FOREIGN KEY');
94 89
            } elseif ($hasConstraintKeyword) {
95 4
                $constraint = $stream->expectName();
96 4
                if ($stream->consume('PRIMARY KEY')) {
97 1
                    $this->parseIndex($stream, 'PRIMARY KEY', $constraint);
98 3
                } elseif ($stream->consume('UNIQUE')) {
99 1
                    $stream->consume('KEY') || $stream->consume('INDEX');
100 1
                    $this->parseIndex($stream, 'UNIQUE KEY', $constraint);
101 2
                } elseif ($stream->consume('FOREIGN KEY')) {
102 1
                    $this->parseIndex($stream, 'FOREIGN KEY', $constraint);
103
                } else {
104 4
                    throw new RuntimeException("Bad CONSTRAINT");
105
                }
106
            } else {
107 89
                $this->parseColumn($stream);
108
            }
109 86
            $token = $stream->nextToken();
110 86
            if ($token->eq(Token::SYMBOL, ',')) {
111 61
                continue;
112 80
            } elseif ($token->eq(Token::SYMBOL, ')')) {
113 79
                break;
114
            } else {
115 1
                throw new RuntimeException("Expected ',' or ')'");
116
            }
117
        }
118
119 79
        $this->processTimestamps();
120 79
        $this->processIndexes();
121 75
        $this->processAutoIncrement();
122 73
        $this->parseTableOptions($stream);
123 73
        $this->processColumnCollations();
124 73
    }
125
126
    /**
127
     * Returns the table's collation.
128
     *
129
     * @return CollationInfo
130
     */
131 73
    public function getCollation()
132
    {
133 73
        return $this->options->collation;
134
    }
135
136
    /**
137
     * Returns an array of SQL DDL statements to create the table.
138
     *
139
     * @return array
140
     */
141 53
    public function getDDL()
142
    {
143 53
        $lines = [];
144 53
        foreach ($this->columns as $column) {
145 53
            $lines[] = "  " . $column->toString($this->getCollation());
146
        }
147 53
        foreach ($this->indexes as $index) {
148 35
            $lines[] = "  " . $index->toString();
149
        }
150 53
        foreach ($this->foreigns as $foreign) {
151 9
            $lines[] = "  " . $foreign->toString();
152
        }
153
154 53
        $text = "CREATE TABLE " . Token::escapeIdentifier($this->name) . " (\n" .
155 53
            implode(",\n", $lines) .
156 53
            "\n" .
157 53
            ")";
158
159 53
        $options = $this->options->toString();
160 53
        if ($options !== '') {
161 53
            $text .= " " . $this->options->toString();
162
        }
163
164 53
        return [$text];
165
    }
166
167
    /**
168
     * @param TokenStream $stream
169
     */
170 89
    private function parseColumn(TokenStream $stream)
171
    {
172 89
        $column = new ColumnDefinition();
173 89
        $column->parse($stream);
174 86
        if (array_key_exists(strtolower($column->name), $this->columns)) {
175 2
            throw new RuntimeException("Duplicate column name '" . $column->name . "'");
176
        }
177 86
        $this->columns[strtolower($column->name)] = $column;
178 86
        $this->indexes = array_merge(
179 86
            $this->indexes,
180 86
            $column->indexes
181
        );
182 86
    }
183
184
    /**
185
     * @param TokenStream $stream
186
     * @param string $type
187
     * @param string|null $constraint
188
     */
189 38
    private function parseIndex(TokenStream $stream, $type, $constraint = null)
190
    {
191 38
        $index = new IndexDefinition();
192 38
        $index->parse($stream, $type, $constraint);
193 38
        $this->indexes[] = $index;
194 38
    }
195
196
    /**
197
     * @param TokenStream $stream
198
     */
199 73
    private function parseTableOptions(TokenStream $stream)
200
    {
201 73
        $this->options->parse($stream);
202 73
    }
203
204 79
    private function processTimestamps()
205
    {
206
        // To specify automatic properties, use the DEFAULT CURRENT_TIMESTAMP
207
        // and ON UPDATE CURRENT_TIMESTAMP clauses. The order of the clauses
208
        // does not matter. If both are present in a column definition, either
209
        // can occur first.
210
211
        // collect all timestamps
212 79
        $ts = [];
213 79
        foreach ($this->columns as $column) {
214 79
            if ($column->type === 'timestamp') {
215 79
                $ts[] = $column;
216
            }
217
        }
218 79
        if (count($ts) === 0) {
219 67
            return;
220
        }
221
222
        // none of NULL, DEFAULT or ON UPDATE CURRENT_TIMESTAMP have been specified
223 12
        if (!$ts[0]->nullable && is_null($ts[0]->default) && !$ts[0]->onUpdateCurrentTimestamp) {
224 3
            $ts[0]->nullable = false;
225 3
            $ts[0]->default = 'CURRENT_TIMESTAMP';
226 3
            $ts[0]->onUpdateCurrentTimestamp = true;
227
        }
228
229
        // [[ this restriction no longer exists as of MySQL 5.6.5 and MariaDB 10.0.1 ]]
230
231
        // One TIMESTAMP column in a table can have the current timestamp as
232
        // the default value for initializing the column, as the auto-update
233
        // value, or both. It is not possible to have the current timestamp
234
        // be the default value for one column and the auto-update value for
235
        // another column.
236
237
        // $specials = 0;
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
238
        // foreach($ts as $column) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
239
        //     if ($column->default === 'CURRENT_TIMESTAMP' ||
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
240
        //         $column->onUpdateCurrentTimestamp
241
        //     ) {
242
        //         if (++$specials > 1) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
243
        //             throw new RuntimeException("There can be only one TIMESTAMP column with CURRENT_TIMESTAMP in DEFAULT or ON UPDATE clause");
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
244
        //         }
245
        //     }
246
        // }
247
248 12
        foreach ($ts as $column) {
249 12
            if (!$column->nullable && is_null($column->default)) {
250 12
                $column->default = '0000-00-00 00:00:00';
251
            }
252
        }
253 12
    }
254
255 79
    private function processIndexes()
256
    {
257
        // check indexes are sane wrt available columns
258 79
        foreach ($this->indexes as $index) {
259 46
            foreach ($index->columns as $indexColumn) {
260 46
                $indexColumnName = $indexColumn['name'];
261 46
                if (!array_key_exists(strtolower($indexColumnName), $this->columns)) {
262 46
                    throw new RuntimeException("Key column '$indexColumnName' doesn't exist in table");
263
                }
264
            }
265
        }
266
267
        // figure out all sequences of columns covered by non-FK indexes
268 78
        foreach ($this->indexes as $index) {
269 45
            if ($index->type !== 'FOREIGN KEY') {
270 41
                foreach ($index->getCovers() as $cover) {
271 41
                    $lookup = implode('\0', $cover);
272 45
                    $this->covers[$lookup] = true;
273
                }
274
            }
275
        }
276
277 78
        $indexes = [];
278 78
        $foreigns = [];
279 78
        $ibfkCounter = 0;
280
281 78
        foreach ($this->indexes as $index) {
282 45
            if ($index->type === 'FOREIGN KEY') {
283
                // TODO - doesn't correctly deal with indexes like foo(10)
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
284 9
                $lookup = implode('\0', $index->getColumns());
285 9
                if (!array_key_exists($lookup, $this->covers)) {
286 7
                    $newIndex = new IndexDefinition();
287 7
                    $newIndex->type = 'KEY';
288 7
                    $newIndex->columns = $index->columns;
289 7
                    if (!is_null($index->constraint)) {
290 1
                        $newIndex->name = $index->constraint;
291 6
                    } elseif (!is_null($index->name)) {
292 1
                        $newIndex->name = $index->name;
293
                    }
294 7
                    $indexes[] = $newIndex;
295
                }
296 9
                $foreign = new IndexDefinition();
297 9
                if (is_null($index->constraint)) {
298 8
                    $foreign->constraint = $this->name . '_ibfk_' . ++$ibfkCounter;
0 ignored issues
show
Coding Style introduced by
Increment and decrement operators must be bracketed when used in string concatenation
Loading history...
299
                } else {
300 1
                    $foreign->constraint = $index->constraint;
301
                }
302 9
                $foreign->type = 'FOREIGN KEY';
303 9
                $foreign->columns = $index->columns;
304 9
                $foreign->reference = $index->reference;
305 9
                $foreigns[] = $foreign;
306
            } else {
307 45
                $indexes[] = $index;
308
            }
309
        }
310
311
        // now synthesise names for any unnamed indexes,
312
        // and collect indexes by type
313 78
        $usedName = [];
314
        $keyTypes = [
315 78
            'PRIMARY KEY',
316
            'UNIQUE KEY',
317
            'KEY',
318
            'FULLTEXT KEY',
319
            'FOREIGN KEY',
320
        ];
321 78
        $indexesByType = array_fill_keys($keyTypes, []);
322 78
        foreach ($indexes as $index) {
323 45
            $name = $index->name;
324 45
            if ($index->type === 'PRIMARY KEY') {
325 19
                $name = 'PRIMARY';
326 27
            } elseif (is_null($name)) {
327 16
                $base = $index->columns[0]['name'];
328 16
                $name = $base;
329 16
                $i = 1;
330 16
                while (isset($usedName[$name])) {
331 1
                    $name = $base . '_' . ++$i;
0 ignored issues
show
Coding Style introduced by
Increment and decrement operators must be bracketed when used in string concatenation
Loading history...
332
                }
333 16
                $index->name = $name;
334 14
            } elseif (array_key_exists(strtolower($name), $usedName)) {
335 2
                throw new RuntimeException("Duplicate key name '$name'");
336
            }
337 45
            $index->name = $name;
338 45
            $usedName[strtolower($name)] = true;
339
340 45
            $indexesByType[$index->type][] = $index;
341
        }
342
343 76
        if (count($indexesByType['PRIMARY KEY']) > 1) {
344 1
            throw new RuntimeException("Multiple PRIMARY KEYs defined");
345
        }
346
347 75
        foreach ($indexesByType['PRIMARY KEY'] as $pk) {
348 18
            foreach ($pk->columns as $indexColumn) {
349 18
                $column = $this->columns[strtolower($indexColumn['name'])];
350 18
                if ($column->nullable) {
351 10
                    $column->nullable = false;
352 10
                    if (is_null($column->default)) {
353 18
                        $column->default = $column->getUninitialisedValue();
354
                    }
355
                }
356
            }
357
        }
358
359 75
        $this->indexes = [];
360 75
        foreach (array_reduce($indexesByType, 'array_merge', []) as $index) {
361 42
            $this->indexes[$index->name] = $index;
362
        }
363 75
        foreach ($foreigns as $foreign) {
364 9
            $this->foreigns[] = $foreign;
365
        }
366 75
    }
367
368 75
    private function processAutoIncrement()
369
    {
370 75
        $count = 0;
371 75
        foreach ($this->columns as $column) {
372 75
            if ($column->autoIncrement) {
373 3
                if (++$count > 1) {
374 1
                    throw new RuntimeException("There can be only one AUTO_INCREMENT column");
375
                }
376 3
                if (! array_key_exists($column->name, $this->covers)) {
377 75
                    throw new RuntimeException("AUTO_INCREMENT column must be defined as a key");
378
                }
379
            }
380
        }
381 73
    }
382
383 73
    private function processColumnCollations()
384
    {
385 73
        foreach ($this->columns as $column) {
386 73
            $column->applyTableCollation($this->getCollation());
387
        }
388 73
    }
389
390
    /**
391
     * Returns ALTER TABLE statement to transform this table into the one
392
     * represented by $that. If the tables are already equivalent, just
393
     * returns the empty string.
394
     *
395
     * $flags        |
396
     * :-------------|----
397
     * 'alterEngine' | (bool) include ALTER TABLE ... ENGINE= [default: true]
398
     *
399
     * @param CreateTable $that
400
     * @param array $flags
401
     * @return string[]
402
     */
403 20
    public function diff(CreateTable $that, array $flags = [])
404
    {
405
        $flags += [
406 20
            'alterEngine' => true
407
        ];
408
409 20
        $alters = array_merge(
410 20
            $this->diffColumns($that),
411 20
            $this->diffIndexes($that),
412 20
            $this->diffOptions($that, [
413 20
                'alterEngine' => $flags['alterEngine']
414
            ])
415
        );
416
417 20
        if (count($alters) === 0) {
418 2
            return [];
419
        }
420
421 18
        return ["ALTER TABLE " . Token::escapeIdentifier($this->name) . "\n" . implode(",\n", $alters)];
422
    }
423
424
    /**
425
     * @param CreateTable $that
426
     * @return array
427
     */
428 20
    private function diffColumns(CreateTable $that)
429
    {
430 20
        $alters = [];
431 20
        $permutation = [];
432 20
        foreach (array_keys($this->columns) as $columnName) {
433 20
            if (array_key_exists($columnName, $that->columns)) {
434 20
                $permutation[] = $columnName;
435
            } else {
436 20
                $alters[] = "DROP COLUMN " . Token::escapeIdentifier($columnName);
437
            }
438
        }
439
440 20
        $prevColumn = null;
441 20
        $thatPosition = " FIRST";
442 20
        $j = 0;
443 20
        foreach ($that->columns as $columnName => $column) {
444 20
            if (array_key_exists($columnName, $this->columns)) {
445
                // An existing column is being changed
446 20
                $thisDefinition = $this->columns[$columnName]->toString($this->getCollation());
447 20
                $thatDefinition = $that->columns[$columnName]->toString($that->getCollation());
448
449
                // about to 'add' $columnName - get its location in the currently
450
                // permuted state of the tabledef
451 20
                $i = array_search($columnName, $permutation);
452
453
                // figure out the column it currently sits after, in case we
454
                // need to change it
455 20
                $thisPosition = ($i === 0) ? " FIRST" : " AFTER " . Token::escapeIdentifier($permutation[$i - 1]);
456
457 20
                if ($thisDefinition !== $thatDefinition ||
458 20
                    $thisPosition   !== $thatPosition
459
                ) {
460 8
                    $alter = "MODIFY COLUMN " . $thatDefinition;
461
462
                    // position has changed
463 8
                    if ($thisPosition !== $thatPosition) {
464 3
                        $alter .= $thatPosition;
465
466
                        // We need to update our permutation to reflect the new position.
467
                        // Column is being inserted at position $j, and is currently residing at $i.
468
469
                        // remove from current location
470 3
                        array_splice($permutation, $i, 1, []);
0 ignored issues
show
Bug introduced by
It seems like $i can also be of type string and false; however, parameter $offset of array_splice() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

470
                        array_splice($permutation, /** @scrutinizer ignore-type */ $i, 1, []);
Loading history...
471
472
                        // insert at new location
473 3
                        array_splice($permutation, $j, 0, $columnName);
474
                    }
475
476 20
                    $alters[] = $alter;
477
                }
478
            } else {
479
                // A new column is being added
480 3
                $alter = "ADD COLUMN " . $column->toString($this->getCollation());
481 3
                if ($j < count($permutation)) {
482 2
                    $alter .= $thatPosition;
483
                }
484 3
                $alters[] = $alter;
485
486 3
                $i = is_null($prevColumn) ? 0 : 1 + array_search($prevColumn, $permutation);
487 3
                array_splice($permutation, $i, 0, [$columnName]);
488
            }
489
490 20
            $prevColumn = $columnName;
491 20
            $thatPosition = " AFTER " . Token::escapeIdentifier($prevColumn);
492 20
            $j++;
493
        }
494
495 20
        return $alters;
496
    }
497
498
    /**
499
     * @param CreateTable $that
500
     * @return array
501
     */
502 20
    private function diffIndexes(CreateTable $that)
503
    {
504 20
        $alters = [];
505
506 20
        foreach ($this->indexes as $indexName => $index) {
507 4
            if (!array_key_exists($indexName, $that->indexes) ||
508 4
                $index->toString() !== $that->indexes[$indexName]->toString()
509
            ) {
510 4
                switch ($index->type) {
511 4
                    case 'PRIMARY KEY':
512 1
                        $alter = "DROP PRIMARY KEY";
513 1
                        break;
514
515
                // TODO - foreign keys???
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
516
517
                    default:
518 3
                        $alter = "DROP KEY " . Token::escapeIdentifier($indexName);
519 3
                        break;
520
                }
521 4
                $alters[] = $alter;
522
            }
523
        }
524
525 20
        foreach ($that->indexes as $indexName => $index) {
526 4
            if (!array_key_exists($indexName, $this->indexes) ||
527 4
                $index->toString() !== $this->indexes[$indexName]->toString()
528
            ) {
529 4
                $alters[] = "ADD " . $index->toString();
530
            }
531
        }
532
533 20
        return $alters;
534
    }
535
536
    /**
537
     * @param CreateTable $that
538
     * @param array $flags
539
     * @return array
540
     */
541 20
    private function diffOptions(CreateTable $that, array $flags = [])
542
    {
543
        $flags += [
544 20
            'alterEngine' => true
545
        ];
546 20
        $diff = $this->options->diff($that->options, [
547 20
            'alterEngine' => $flags['alterEngine']
548
        ]);
549 20
        return ($diff == '') ? [] : [$diff];
550
    }
551
}
552