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 ( 79124e...156f58 )
by Burhan
02:39
created

CreateTable::getName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 TableOptions */
24
    public $options = null;
25
26
    /** @var array */
27
    private $covers = [];
28
29
    /**
30
     * Constructor.
31
     *
32
     * @param CollationInfo $databaseCollation
33
     */
34 93
    public function __construct(CollationInfo $databaseCollation)
35
    {
36 93
        $this->options = new TableOptions($databaseCollation);
37 93
    }
38
39
    /**
40
     * @return string
41
     */
42 1
    public function getName()
43
    {
44 1
        return $this->name;
45
    }
46
47
    /**
48
     * Sets the storage engine the table is assumed to use, unless
49
     * explicitly overridden via an ENGINE= clause at the end of
50
     * the table definition.
51
     *
52
     * @param string $engine
53
     * @return void
54
     */
55 91
    public function setDefaultEngine($engine)
56
    {
57 91
        $this->options->setDefaultEngine($engine);
58 91
    }
59
60
    /**
61
     * Parses a table definition from $stream.
62
     *
63
     * The DDL may be of the form 'CREATE TABLE ...' or 'CREATE TABLE IF NOT EXISTS ...'.
64
     *
65
     * An exception will be thrown if a valid CREATE TABLE statement cannot be recognised.
66
     *
67
     * @param TokenStream $stream
68
     */
69 90
    public function parse(TokenStream $stream)
70
    {
71 90
        if ($stream->consume('CREATE TABLE')) {
72 89
            $stream->consume('IF NOT EXISTS');
73
        } else {
74 1
            throw new RuntimeException("Expected CREATE TABLE");
75
        }
76
77 89
        $this->name = $stream->expectName();
78 89
        $stream->expectOpenParen();
79
80 89
        while (true) {
81 89
            $hasConstraintKeyword = $stream->consume('CONSTRAINT');
82 89
            if ($stream->consume('PRIMARY KEY')) {
83 10
                $this->parseIndex($stream, 'PRIMARY KEY');
84 89
            } elseif ($stream->consume('KEY') ||
85 89
                $stream->consume('INDEX')
86
            ) {
87 18
                if ($hasConstraintKeyword) {
88 2
                    throw new RuntimeException("Bad CONSTRAINT");
89
                }
90 16
                $this->parseIndex($stream, 'KEY');
91 89
            } elseif ($stream->consume('FULLTEXT')) {
92 4
                if ($hasConstraintKeyword) {
93 1
                    throw new RuntimeException("Bad CONSTRAINT");
94
                }
95 3
                $stream->consume('KEY') || $stream->consume('INDEX');
96 3
                $this->parseIndex($stream, 'FULLTEXT KEY');
97 89
            } elseif ($stream->consume('UNIQUE')) {
98 5
                $stream->consume('KEY') || $stream->consume('INDEX');
99 5
                $this->parseIndex($stream, 'UNIQUE KEY');
100 89
            } elseif ($stream->consume('FOREIGN KEY')) {
101 8
                $this->parseIndex($stream, 'FOREIGN KEY');
102 89
            } elseif ($hasConstraintKeyword) {
103 4
                $constraint = $stream->expectName();
104 4
                if ($stream->consume('PRIMARY KEY')) {
105 1
                    $this->parseIndex($stream, 'PRIMARY KEY', $constraint);
106 3
                } elseif ($stream->consume('UNIQUE')) {
107 1
                    $stream->consume('KEY') || $stream->consume('INDEX');
108 1
                    $this->parseIndex($stream, 'UNIQUE KEY', $constraint);
109 2
                } elseif ($stream->consume('FOREIGN KEY')) {
110 1
                    $this->parseIndex($stream, 'FOREIGN KEY', $constraint);
111
                } else {
112 4
                    throw new RuntimeException("Bad CONSTRAINT");
113
                }
114
            } else {
115 89
                $this->parseColumn($stream);
116
            }
117 86
            $token = $stream->nextToken();
118 86
            if ($token->eq(Token::SYMBOL, ',')) {
119 61
                continue;
120 80
            } elseif ($token->eq(Token::SYMBOL, ')')) {
121 79
                break;
122
            } else {
123 1
                throw new RuntimeException("Expected ',' or ')'");
124
            }
125
        }
126
127 79
        $this->processTimestamps();
128 79
        $this->processIndexes();
129 75
        $this->processAutoIncrement();
130 73
        $this->parseTableOptions($stream);
131 73
        $this->processColumnCollations();
132 73
    }
133
134
    /**
135
     * Returns the table's collation.
136
     *
137
     * @return CollationInfo
138
     */
139 73
    public function getCollation()
140
    {
141 73
        return $this->options->collation;
142
    }
143
144
    /**
145
     * Returns an array of SQL DDL statements to create the table.
146
     *
147
     * @return array
148
     */
149 53
    public function getDDL()
150
    {
151 53
        $lines = [];
152 53
        foreach ($this->columns as $column) {
153 53
            $lines[] = "  " . $column->toString($this->getCollation());
154
        }
155 53
        foreach ($this->indexes as $index) {
156 35
            $lines[] = "  " . $index->toString();
157
        }
158 53
        foreach ($this->foreigns as $foreign) {
159 9
            $lines[] = "  " . $foreign->toString();
160
        }
161
162 53
        $text = "CREATE TABLE " . Token::escapeIdentifier($this->name) . " (\n" .
163 53
            implode(",\n", $lines) .
164 53
            "\n" .
165 53
            ")";
166
167 53
        $options = $this->options->toString();
168 53
        if ($options !== '') {
169 53
            $text .= " " . $this->options->toString();
170
        }
171
172 53
        return [$text];
173
    }
174
175
    /**
176
     * @param TokenStream $stream
177
     */
178 89
    private function parseColumn(TokenStream $stream)
179
    {
180 89
        $column = new ColumnDefinition();
181 89
        $column->parse($stream);
182 86
        if (array_key_exists(strtolower($column->name), $this->columns)) {
183 2
            throw new RuntimeException("Duplicate column name '" . $column->name . "'");
184
        }
185 86
        $this->columns[strtolower($column->name)] = $column;
186 86
        $this->indexes = array_merge(
187 86
            $this->indexes,
188 86
            $column->indexes
189
        );
190 86
    }
191
192
    /**
193
     * @param TokenStream $stream
194
     * @param string $type
195
     * @param string|null $constraint
196
     */
197 38
    private function parseIndex(TokenStream $stream, $type, $constraint = null)
198
    {
199 38
        $index = new IndexDefinition();
200 38
        $index->parse($stream, $type, $constraint);
201 38
        $this->indexes[] = $index;
202 38
    }
203
204
    /**
205
     * @param TokenStream $stream
206
     */
207 73
    private function parseTableOptions(TokenStream $stream)
208
    {
209 73
        $this->options->parse($stream);
210 73
    }
211
212 79
    private function processTimestamps()
213
    {
214
        // To specify automatic properties, use the DEFAULT CURRENT_TIMESTAMP
215
        // and ON UPDATE CURRENT_TIMESTAMP clauses. The order of the clauses
216
        // does not matter. If both are present in a column definition, either
217
        // can occur first.
218
219
        // collect all timestamps
220 79
        $ts = [];
221 79
        foreach ($this->columns as $column) {
222 79
            if ($column->type === 'timestamp') {
223 12
                $ts[] = $column;
224
            }
225
        }
226 79
        if (count($ts) === 0) {
227 67
            return;
228
        }
229
230
        // none of NULL, DEFAULT or ON UPDATE CURRENT_TIMESTAMP have been specified
231 12
        if (!$ts[0]->nullable && is_null($ts[0]->default) && !$ts[0]->onUpdateCurrentTimestamp) {
232 3
            $ts[0]->nullable = false;
233 3
            $ts[0]->default = 'CURRENT_TIMESTAMP';
234 3
            $ts[0]->onUpdateCurrentTimestamp = true;
235
        }
236
237
        // [[ this restriction no longer exists as of MySQL 5.6.5 and MariaDB 10.0.1 ]]
238
239
        // One TIMESTAMP column in a table can have the current timestamp as
240
        // the default value for initializing the column, as the auto-update
241
        // value, or both. It is not possible to have the current timestamp
242
        // be the default value for one column and the auto-update value for
243
        // another column.
244
245
        // $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...
246
        // 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...
247
        //     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...
248
        //         $column->onUpdateCurrentTimestamp
249
        //     ) {
250
        //         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...
251
        //             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...
252
        //         }
253
        //     }
254
        // }
255
256 12
        foreach ($ts as $column) {
257 12
            if (!$column->nullable && is_null($column->default)) {
258 4
                $column->default = '0000-00-00 00:00:00';
259
            }
260
        }
261 12
    }
262
263 79
    private function processIndexes()
264
    {
265
        // check indexes are sane wrt available columns
266 79
        foreach ($this->indexes as $index) {
267 46
            foreach ($index->columns as $indexColumn) {
268 46
                $indexColumnName = $indexColumn['name'];
269 46
                if (!array_key_exists(strtolower($indexColumnName), $this->columns)) {
270 1
                    throw new RuntimeException("Key column '$indexColumnName' doesn't exist in table");
271
                }
272
            }
273
        }
274
275
        // figure out all sequences of columns covered by non-FK indexes
276 78
        foreach ($this->indexes as $index) {
277 45
            if ($index->type !== 'FOREIGN KEY') {
278 41
                foreach ($index->getCovers() as $cover) {
279 41
                    $lookup = implode('\0', $cover);
280 41
                    $this->covers[$lookup] = true;
281
                }
282
            }
283
        }
284
285 78
        $indexes = [];
286 78
        $foreigns = [];
287 78
        $ibfkCounter = 0;
288
289 78
        foreach ($this->indexes as $index) {
290 45
            if ($index->type === 'FOREIGN KEY') {
291
                // 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...
292 9
                $lookup = implode('\0', $index->getColumns());
293 9
                if (!array_key_exists($lookup, $this->covers)) {
294 7
                    $newIndex = new IndexDefinition();
295 7
                    $newIndex->type = 'KEY';
296 7
                    $newIndex->columns = $index->columns;
297 7
                    if (!is_null($index->constraint)) {
298 1
                        $newIndex->name = $index->constraint;
299 6
                    } elseif (!is_null($index->name)) {
300 1
                        $newIndex->name = $index->name;
301
                    }
302 7
                    $indexes[] = $newIndex;
303
                }
304 9
                $foreign = new IndexDefinition();
305 9
                if (is_null($index->constraint)) {
306 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...
307
                } else {
308 1
                    $foreign->constraint = $index->constraint;
309
                }
310 9
                $foreign->type = 'FOREIGN KEY';
311 9
                $foreign->columns = $index->columns;
312 9
                $foreign->reference = $index->reference;
313 9
                $foreigns[] = $foreign;
314
            } else {
315 41
                $indexes[] = $index;
316
            }
317
        }
318
319
        // now synthesise names for any unnamed indexes,
320
        // and collect indexes by type
321 78
        $usedName = [];
322
        $keyTypes = [
323 78
            'PRIMARY KEY',
324
            'UNIQUE KEY',
325
            'KEY',
326
            'FULLTEXT KEY',
327
            'FOREIGN KEY',
328
        ];
329 78
        $indexesByType = array_fill_keys($keyTypes, []);
330 78
        foreach ($indexes as $index) {
331 45
            $name = $index->name;
332 45
            if ($index->type === 'PRIMARY KEY') {
333 19
                $name = 'PRIMARY';
334 27
            } elseif (is_null($name)) {
335 16
                $base = $index->columns[0]['name'];
336 16
                $name = $base;
337 16
                $i = 1;
338 16
                while (isset($usedName[$name])) {
339 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...
340
                }
341 16
                $index->name = $name;
342 14
            } elseif (array_key_exists(strtolower($name), $usedName)) {
343 2
                throw new RuntimeException("Duplicate key name '$name'");
344
            }
345 45
            $index->name = $name;
346 45
            $usedName[strtolower($name)] = true;
347
348 45
            $indexesByType[$index->type][] = $index;
349
        }
350
351 76
        if (count($indexesByType['PRIMARY KEY']) > 1) {
352 1
            throw new RuntimeException("Multiple PRIMARY KEYs defined");
353
        }
354
355 75
        foreach ($indexesByType['PRIMARY KEY'] as $pk) {
356 18
            foreach ($pk->columns as $indexColumn) {
357 18
                $column = $this->columns[strtolower($indexColumn['name'])];
358 18
                if ($column->nullable) {
359 10
                    $column->nullable = false;
360 10
                    if (is_null($column->default)) {
361 10
                        $column->default = $column->getUninitialisedValue();
362
                    }
363
                }
364
            }
365
        }
366
367 75
        $this->indexes = [];
368 75
        foreach (array_reduce($indexesByType, 'array_merge', []) as $index) {
369 42
            $this->indexes[$index->name] = $index;
370
        }
371 75
        foreach ($foreigns as $foreign) {
372 9
            $this->foreigns[] = $foreign;
373
        }
374 75
    }
375
376 75
    private function processAutoIncrement()
377
    {
378 75
        $count = 0;
379 75
        foreach ($this->columns as $column) {
380 75
            if ($column->autoIncrement) {
381 3
                if (++$count > 1) {
382 1
                    throw new RuntimeException("There can be only one AUTO_INCREMENT column");
383
                }
384 3
                if (! array_key_exists($column->name, $this->covers)) {
385 1
                    throw new RuntimeException("AUTO_INCREMENT column must be defined as a key");
386
                }
387
            }
388
        }
389 73
    }
390
391 73
    private function processColumnCollations()
392
    {
393 73
        foreach ($this->columns as $column) {
394 73
            $column->applyTableCollation($this->getCollation());
395
        }
396 73
    }
397
398
    /**
399
     * Returns ALTER TABLE statement to transform this table into the one
400
     * represented by $that. If the tables are already equivalent, just
401
     * returns the empty string.
402
     *
403
     * $flags        |
404
     * :-------------|----
405
     * 'alterEngine' | (bool) include ALTER TABLE ... ENGINE= [default: true]
406
     *
407
     * @param CreateTable $that
408
     * @param array $flags
409
     * @return string[]
410
     */
411 20
    public function diff(CreateTable $that, array $flags = [])
412
    {
413
        $flags += [
414 20
            'alterEngine' => true
415
        ];
416
417 20
        $alters = array_merge(
418 20
            $this->diffColumns($that),
419 20
            $this->diffIndexes($that),
420 20
            $this->diffOptions($that, [
421 20
                'alterEngine' => $flags['alterEngine']
422
            ])
423
        );
424
425 20
        if (count($alters) === 0) {
426 2
            return [];
427
        }
428
429 18
        return ["ALTER TABLE " . Token::escapeIdentifier($this->name) . "\n" . implode(",\n", $alters)];
430
    }
431
432
    /**
433
     * @param CreateTable $that
434
     * @return array
435
     */
436 20
    private function diffColumns(CreateTable $that)
437
    {
438 20
        $alters = [];
439 20
        $permutation = [];
440 20
        foreach (array_keys($this->columns) as $columnName) {
441 20
            if (array_key_exists($columnName, $that->columns)) {
442 20
                $permutation[] = $columnName;
443
            } else {
444 1
                $alters[] = "DROP COLUMN " . Token::escapeIdentifier($columnName);
445
            }
446
        }
447
448 20
        $prevColumn = null;
449 20
        $thatPosition = " FIRST";
450 20
        $j = 0;
451 20
        foreach ($that->columns as $columnName => $column) {
452 20
            if (array_key_exists($columnName, $this->columns)) {
453
                // An existing column is being changed
454 20
                $thisDefinition = $this->columns[$columnName]->toString($this->getCollation());
455 20
                $thatDefinition = $that->columns[$columnName]->toString($that->getCollation());
456
457
                // about to 'add' $columnName - get its location in the currently
458
                // permuted state of the tabledef
459 20
                $i = array_search($columnName, $permutation);
460
461
                // figure out the column it currently sits after, in case we
462
                // need to change it
463 20
                $thisPosition = ($i === 0) ? " FIRST" : " AFTER " . Token::escapeIdentifier($permutation[$i - 1]);
464
465 20
                if ($thisDefinition !== $thatDefinition ||
466 20
                    $thisPosition   !== $thatPosition
467
                ) {
468 8
                    $alter = "MODIFY COLUMN " . $thatDefinition;
469
470
                    // position has changed
471 8
                    if ($thisPosition !== $thatPosition) {
472 3
                        $alter .= $thatPosition;
473
474
                        // We need to update our permutation to reflect the new position.
475
                        // Column is being inserted at position $j, and is currently residing at $i.
476
477
                        // remove from current location
478 3
                        array_splice($permutation, $i, 1, []);
0 ignored issues
show
Bug introduced by
It seems like $i can also be of type false and string; 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

478
                        array_splice($permutation, /** @scrutinizer ignore-type */ $i, 1, []);
Loading history...
479
480
                        // insert at new location
481 3
                        array_splice($permutation, $j, 0, $columnName);
482
                    }
483
484 20
                    $alters[] = $alter;
485
                }
486
            } else {
487
                // A new column is being added
488 3
                $alter = "ADD COLUMN " . $column->toString($this->getCollation());
489 3
                if ($j < count($permutation)) {
490 2
                    $alter .= $thatPosition;
491
                }
492 3
                $alters[] = $alter;
493
494 3
                $i = is_null($prevColumn) ? 0 : 1 + array_search($prevColumn, $permutation);
495 3
                array_splice($permutation, $i, 0, [$columnName]);
496
            }
497
498 20
            $prevColumn = $columnName;
499 20
            $thatPosition = " AFTER " . Token::escapeIdentifier($prevColumn);
500 20
            $j++;
501
        }
502
503 20
        return $alters;
504
    }
505
506
    /**
507
     * @param CreateTable $that
508
     * @return array
509
     */
510 20
    private function diffIndexes(CreateTable $that)
511
    {
512 20
        $alters = [];
513
514 20
        foreach ($this->indexes as $indexName => $index) {
515 4
            if (!array_key_exists($indexName, $that->indexes) ||
516 4
                $index->toString() !== $that->indexes[$indexName]->toString()
517
            ) {
518 4
                switch ($index->type) {
519 4
                    case 'PRIMARY KEY':
520 1
                        $alter = "DROP PRIMARY KEY";
521 1
                        break;
522
523
                // 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...
524
525
                    default:
526 3
                        $alter = "DROP KEY " . Token::escapeIdentifier($indexName);
527 3
                        break;
528
                }
529 4
                $alters[] = $alter;
530
            }
531
        }
532
533 20
        foreach ($that->indexes as $indexName => $index) {
534 4
            if (!array_key_exists($indexName, $this->indexes) ||
535 4
                $index->toString() !== $this->indexes[$indexName]->toString()
536
            ) {
537 4
                $alters[] = "ADD " . $index->toString();
538
            }
539
        }
540
541 20
        return $alters;
542
    }
543
544
    /**
545
     * @param CreateTable $that
546
     * @param array $flags
547
     * @return array
548
     */
549 20
    private function diffOptions(CreateTable $that, array $flags = [])
550
    {
551
        $flags += [
552 20
            'alterEngine' => true
553
        ];
554 20
        $diff = $this->options->diff($that->options, [
555 20
            'alterEngine' => $flags['alterEngine']
556
        ]);
557 20
        return ($diff == '') ? [] : [$diff];
558
    }
559
}
560