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.

Issues (23)

src/Parse/ColumnDefinition.php (4 issues)

1
<?php
2
namespace Graze\Morphism\Parse;
3
4
use Exception;
5
use RuntimeException;
6
7
/**
8
 * Represents the definition of a single column in a table.
9
 */
10
class ColumnDefinition
11
{
12
    /** @var string */
13
    public $name = '';
14
15
    /** @var string */
16
    public $type = '';
17
18
    /** @var int */
19
    public $length = null;
20
21
    /** @var int */
22
    public $decimals = 0;
23
24
    /** @var bool */
25
    public $unsigned = false;
26
27
    /** @var bool */
28
    public $zerofill = false;
29
30
    /** @var string[] */
31
    public $elements = [];
32
33
    /** @var CollationInfo */
34
    public $collation = null;
35
36
    /** @var bool */
37
    public $nullable = true;
38
39
    /** @var bool */
40
    public $autoIncrement = false;
41
42
    /** @var string */
43
    public $default = null;
44
45
    /** @var string|null */
46
    public $comment = null;
47
48
    /** @var bool */
49
    public $onUpdateCurrentTimestamp = false;
50
51
    /** @var IndexDefinition[] */
52
    public $indexes = [];
53
54
    /** @var bool */
55
    private $primaryKey = false;
56
    /** @var bool */
57
    private $uniqueKey = false;
58
59
    /** @var array */
60
    private static $typeInfoMap = [
61
        //                             format   default  allow   allow   allow   uninitialised
62
        // datatype       kind         Spec     Lengths  Autoinc Binary  Charset Value
63
        'bit'        => [ 'bit',       [0,1  ], null,    false,  false,  false,  0,    ],
64
        'tinyint'    => [ 'int',       [0,1  ], null,    true,   false,  false,  0,    ],
65
        'smallint'   => [ 'int',       [0,1  ], null,    true,   false,  false,  0,    ],
66
        'mediumint'  => [ 'int',       [0,1  ], null,    true,   false,  false,  0,    ],
67
        'int'        => [ 'int',       [0,1  ], null,    true,   false,  false,  0,    ],
68
        'bigint'     => [ 'int',       [0,1  ], null,    true,   false,  false,  0,    ],
69
        'double'     => [ 'decimal',   [0,  2], null,    true,   false,  false,  0,    ], // prec = 22
70
        'float'      => [ 'decimal',   [0,  2], null,    true,   false,  false,  0,    ], // prec = 12
71
        'decimal'    => [ 'decimal',   [0,1,2], [10,10], false,  false,  false,  0,    ],
72
        'date'       => [ 'date',      [0    ], null,    false,  false,  false,  0,    ],
73
        'time'       => [ 'time',      [0    ], null,    false,  false,  false,  0,    ],
74
        'timestamp'  => [ 'datetime',  [0    ], null,    false,  false,  false,  0,    ],
75
        'datetime'   => [ 'datetime',  [0,1  ], null,    false,  false,  false,  0,    ],
76
        'year'       => [ 'year',      [0,1  ], [4],     false,  false,  false,  0,    ],
77
        'char'       => [ 'text',      [0,1  ], [1],     false,  true,   true,   '',   ],
78
        'varchar'    => [ 'text',      [  1  ], null,    false,  true,   true,   '',   ],
79
        'binary'     => [ 'binary',    [0,1  ], [1],     false,  false,  false,  '',   ],
80
        'varbinary'  => [ 'text',      [  1  ], null,    false,  false,  false,  '',   ],
81
        'tinyblob'   => [ 'blob',      [0    ], null,    false,  false,  false,  null, ],
82
        'blob'       => [ 'blob',      [0    ], null,    false,  false,  false,  null, ],
83
        'mediumblob' => [ 'blob',      [0    ], null,    false,  false,  false,  null, ],
84
        'longblob'   => [ 'blob',      [0    ], null,    false,  false,  false,  null, ],
85
        'tinytext'   => [ 'blob',      [0    ], null,    false,  true,   true,   null, ],
86
        'text'       => [ 'blob',      [0    ], null,    false,  true,   true,   null, ],
87
        'mediumtext' => [ 'blob',      [0    ], null,    false,  true,   true,   null, ],
88
        'longtext'   => [ 'blob',      [0    ], null,    false,  true,   true,   null, ],
89
        'enum'       => [ 'enum',      [0    ], null,    false,  false,  true,   0,    ],
90
        'set'        => [ 'set',       [0    ], null,    false,  false,  true,   '',   ],
91
        'json'       => [ 'json',      [0    ], null,    false,  false,  false,  null, ],
92
    ];
93
    /** @var array */
94
    private static $typeInfoCache = [];
95
96
    /** @var array */
97
    private static $aliasMap = [
98
        'bool'      => 'tinyint',
99
        'boolean'   => 'tinyint',
100
        'int1'      => 'tinyint',
101
        'int2'      => 'smallint',
102
        'int3'      => 'mediumint',
103
        'middleint' => 'mediumint',
104
        'int4'      => 'int',
105
        'integer'   => 'int',
106
        'int8'      => 'bigint',
107
        'dec'       => 'decimal',
108
        'numeric'   => 'decimal',
109
        'fixed'     => 'decimal',
110
        'real'      => 'double',
111
    ];
112
113
    /**
114
     * Constructor
115
     */
116 324
    public function __construct()
117
    {
118 324
        $this->collation = new CollationInfo();
119 324
    }
120
121
    /**
122
     * Parse column definition from $stream.
123
     *
124
     * An exception will be thrown if a valid column definition cannot be
125
     * recognised.
126
     *
127
     * @param TokenStream $stream
128
     */
129 323
    public function parse(TokenStream $stream)
130
    {
131 323
        $this->name = $stream->expectName();
132 322
        $this->parseColumnDatatype($stream);
133 304
        $this->parseColumnOptions($stream);
134
135 289
        if ($this->primaryKey) {
136 12
            $this->addIndex('PRIMARY KEY');
137
        }
138 289
        if ($this->uniqueKey) {
139 8
            $this->addIndex('UNIQUE KEY');
140
        }
141 289
    }
142
143
    /**
144
     * @param string $type
145
     */
146 20
    private function addIndex($type)
147
    {
148
        // TODO - crying out for an IndexPart class
149 20
        $index = new IndexDefinition();
150 20
        $index->type = $type;
151 20
        $index->columns[] = [
152 20
            'name'   => $this->name,
153
            'length' => null,
154 20
            'sort'   => 'ASC',
155
        ];
156 20
        $this->indexes[] = $index;
157 20
    }
158
159
    /**
160
     * @return object|null
161
     */
162 319
    private function getTypeInfo()
163
    {
164 319
        if (array_key_exists($this->type, self::$typeInfoCache)) {
165 316
            return self::$typeInfoCache[$this->type];
166
        }
167
168 32
        if (!array_key_exists($this->type, self::$typeInfoMap)) {
169 3
            return null;
170
        }
171
172 29
        $data = self::$typeInfoMap[$this->type];
173
174
        $typeInfo = (object) [
175 29
            'kind'               => $data[0],
176 29
            'formatSpec'         => array_fill_keys($data[1], true) + array_fill_keys(range(0, 2), false),
177 29
            'defaultLengths'     => $data[2],
178 29
            'allowAutoIncrement' => $data[3],
179 29
            'allowBinary'        => $data[4],
180 29
            'allowCharset'       => $data[5],
181 29
            'allowDefault'       => !is_null($data[6]),
182 29
            'uninitialisedValue' => $data[6],
183
        ];
184 29
        $typeInfo->allowSign = in_array($typeInfo->kind, ['int', 'decimal']);
185 29
        self::$typeInfoCache[$this->type] = $typeInfo;
186
187 29
        return $typeInfo;
188
    }
189
190
    /**
191
     * @param TokenStream $stream
192
     */
193 322
    private function parseColumnDatatype(TokenStream $stream)
194
    {
195 322
        $token = $stream->nextToken();
196 322
        if ($token->type !== Token::IDENTIFIER) {
197 2
            throw new RuntimeException("Expected a datatype");
198
        }
199
200
        // map aliases to concrete type
201 320
        $sqlType = strtolower($token->text);
202 320
        if (array_key_exists($sqlType, self::$aliasMap)) {
203 39
            $type = self::$aliasMap[$sqlType];
204
        } else {
205 281
            switch ($sqlType) {
206 281
                case 'serial':
207
                    // SERIAL is an alias for  BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE
208 2
                    $this->type = 'bigint';
209 2
                    $this->unsigned = true;
210 2
                    $this->nullable = false;
211 2
                    $this->autoIncrement = true;
212 2
                    $this->uniqueKey = true;
213 2
                    return;
214 2
215
                case 'character':
216 279
                    if ($stream->consume('varying')) {
217 3
                        $sqlType .= ' varying';
218 1
                        $type = 'varchar';
219 1
                    } else {
220
                        $type = 'char';
221 2
                    }
222
                    break;
223 3
224
                case 'double':
225 276
                    $stream->consume('PRECISION');
226 7
                    $sqlType .= ' PRECISION';
227 7
                    $type = 'double';
228 7
                    break;
229 7
230
                case 'long':
231 269
                    if ($stream->consume('varbinary')) {
232 3
                        $sqlType .= ' varbinary';
233 1
                        $type = 'mediumblob';
234 1
                    } elseif ($stream->consume('varchar')) {
235 2
                        $sqlType .= ' varchar';
236 1
                        $type = 'mediumtext';
237 1
                    } else {
238
                        $type = 'mediumtext';
239 1
                    }
240
                    break;
241 3
242
                default:
243
                    $type = $sqlType;
244 266
            }
245
        }
246
247
        $this->type = $type;
248 318
249
        $typeInfo = $this->getTypeInfo();
250 318
        if (is_null($typeInfo)) {
251 318
            throw new RuntimeException("Unknown datatype '$type'");
252 3
        }
253
254
        $format = [];
255 315
256
        switch ($sqlType) {
257 315
            case 'timestamp':
258 315
                $this->nullable = false;
259 28
                break;
260 28
261
            case 'enum':
262 287
            case 'set':
263 280
                $stream->expectOpenParen();
264 15
                while (true) {
265 15
                    $this->elements[] = rtrim($stream->expectStringExtended(), " ");
266 15
                    $token = $stream->nextToken();
267 15
                    if ($token->eq(Token::SYMBOL, ',')) {
268 15
                        continue;
269 14
                    }
270
                    if ($token->eq(Token::SYMBOL, ')')) {
271 15
                        break;
272 14
                    }
273
                    throw new RuntimeException("Expected ',' or ')'");
274 1
                }
275
                break;
276 14
277
            case 'bool':
278 272
            case 'boolean':
279 270
                /* Take a copy so that edits don't make it back into the runtime cache. */
280 4
                $typeInfo = clone $typeInfo;
281
                $typeInfo->allowSign = false;
282 4
                break;
283 4
284 4
            case 'tinyint':
285 4
            case 'smallint':
286
            case 'mediumint':
287
            case 'int':
288 268
            case 'bigint':
289 268
            case 'int1':
290 57
            case 'int2':
291 1
            case 'int3':
292
            case 'int4':
293 56
            case 'int8':
294 56
            case 'middleint':
295 14
            case 'integer':
296 1
                $spec = $typeInfo->formatSpec;
0 ignored issues
show
The assignment to $spec is dead and can be removed.
Loading history...
297
                // Skip for display width spec for integer data types was deprecated after 8.0.19
298 13
                if ($stream->consume([[Token::SYMBOL, '(']])) {
299 42
                    $stream->nextToken();
300 1
                    $stream->nextToken();
301 1
                }
302 1
                break;
303 1
    
304
            default:
305 54
                $spec = $typeInfo->formatSpec;
306 212
                if ($stream->consume([[Token::SYMBOL, '(']])) {
307 1
                    if (!($spec[1] || $spec[2])) {
308
                        throw new RuntimeException("Unexpected '('");
309 264
                    }
310
                    $format[] = $stream->expectNumber();
311
                    if ($stream->consume([[Token::SYMBOL, ',']])) {
312 310
                        if (!$spec[2]) {
313 310
                            throw new RuntimeException("Unexpected ','");
314 310
                        }
315 310
                        $format[] = $stream->expectNumber();
316 172
                    } elseif (!$spec[1]) {
317 172
                        $mark = $stream->getMark();
318
                        $unexpectedToken = $stream->nextToken();
319
                        $stream->rewind($mark);
320 159
                        throw new RuntimeException("Expected ',' but got: '$unexpectedToken->text'");
321 9
                    }
322 1
                    $stream->expectCloseParen();
323
                } elseif (!$spec[0]) {
324 8
                    throw new RuntimeException("Expected '('");
325 154
                }
326 13
                break;
327 1
        }
328
329 12
        while (true) {
330 142
            $mark = $stream->getMark();
331 2
            $token1 = $stream->nextToken();
332 1
            if ($token1->type !== Token::IDENTIFIER) {
333
                $stream->rewind($mark);
334 1
                break;
335
            }
336 140
337 140
            if ($token1->eq(Token::IDENTIFIER, 'ZEROFILL')) {
338
                throw new RuntimeException("Unexpected ZEROFILL");
339
            } elseif ($token1->eq(Token::IDENTIFIER, 'UNSIGNED')) {
340
                if (!$typeInfo->allowSign) {
341 307
                    throw new RuntimeException("Unexpected UNSIGNED");
342 8
                }
343
                $this->unsigned = true;
344
            } elseif ($token1->eq(Token::IDENTIFIER, 'SIGNED')) {
345 307
                if (!$typeInfo->allowSign) {
346 307
                    throw new RuntimeException("Unexpected SIGNED");
347 185
                }
348 137
                $this->unsigned = false;
349 27
            } else {
350
                $stream->rewind($mark);
351 110
                break;
352
            }
353
        }
354
355
        $defaultLengths = $typeInfo->defaultLengths;
356 307
        if (!is_null($defaultLengths)) {
357 194
            if (count($format) === 0) {
358
                if (count($defaultLengths) === 1 || $this->unsigned) {
359 307
                    $format[0] = $defaultLengths[0];
360 13
                } else {
361
                    $format[0] = $defaultLengths[1];
362
                }
363 307
            }
364 2
        }
365
366
        if (array_key_exists(0, $format)) {
367 305
            $this->length = $format[0];
368 305
        }
369 305
        if (array_key_exists(1, $format)) {
370 305
            $this->decimals = $format[1];
371 172
        }
372 172
373
        if ($this->type === 'year' && $this->length !== 4) {
374
            throw new RuntimeException("This tool will only accept 4 as a valid width for YEAR columns");
375 140
        }
376 2
377 1
        while (true) {
378
            $mark = $stream->getMark();
379 1
            $token1 = $stream->nextToken();
380 138
            if ($token1->type !== Token::IDENTIFIER) {
381 138
                $stream->rewind($mark);
382
                break;
383 2
            }
384 1
385
            if ($token1->eq(Token::IDENTIFIER, 'BINARY')) {
386 1
                if (!$typeInfo->allowBinary) {
387 1
                    throw new RuntimeException("Unexpected BINARY");
388
                }
389 136
                $this->collation->setBinaryCollation();
390 136
            } elseif ($token1->eq(Token::IDENTIFIER, 'CHARSET') ||
391
                ($token1->eq(Token::IDENTIFIER, 'CHARACTER') && $stream->consume('SET'))
392
            ) {
393
                if (!$typeInfo->allowCharset) {
394 303
                    throw new RuntimeException("Unexpected CHARSET");
395 2
                }
396 1
                $charset = $stream->expectName();
397
                $this->collation->setCharset($charset);
398 1
            } else {
399 1
                $stream->rewind($mark);
400
                break;
401 302
            }
402
        }
403
404
        if ($stream->consume('COLLATE')) {
405
            if (!$typeInfo->allowCharset) {
406 304
                throw new RuntimeException("Unexpected COLLATE");
407
            }
408 304
            $collation = $stream->expectName();
409 304
            $this->collation->setCollation($collation);
410 304
        }
411 304
    }
412 288
413 288
    /**
414
     * @param TokenStream $stream
415
     */
416 134
    private function parseColumnOptions(TokenStream $stream)
417 134
    {
418
        while (true) {
419 31
            $mark = $stream->getMark();
420 107
            $token1 = $stream->nextToken();
421
            if ($token1->type !== Token::IDENTIFIER) {
422 13
                $stream->rewind($mark);
423 13
                break;
424
            }
425 97
426
            if ($token1->eq(Token::IDENTIFIER, 'NOT') &&
427 64
                $stream->consume('NULL')
428
            ) {
429 64
                $this->nullable = false;
430 59
            } elseif ($token1->eq(Token::IDENTIFIER, 'NULL')
431 50
            ) {
432 64
                if (!$this->autoIncrement) {
433
                    $this->nullable = true;
434 16
                }
435 16
            } elseif ($token1->eq(Token::IDENTIFIER, 'DEFAULT')
436
            ) {
437 1
                $token2 = $stream->nextToken();
438
439 15
                if ($token2->eq(Token::IDENTIFIER, 'NOW') ||
440
                    $token2->eq(Token::IDENTIFIER, 'CURRENT_TIMESTAMP') ||
441
                    $token2->eq(Token::IDENTIFIER, 'LOCALTIME') ||
442
                    $token2->eq(Token::IDENTIFIER, 'LOCALTIMESTAMP')
443 63
                ) {
444 10
                    if (!$stream->consume([[Token::SYMBOL, '('], [Token::SYMBOL, ')']]) &&
445 63
                        $token2->eq(Token::IDENTIFIER, 'NOW')
446
                    ) {
447 39
                        throw new RuntimeException("Expected () after keyword NOW");
448 39
                    }
449
                    $token2 = new Token(Token::IDENTIFIER, 'CURRENT_TIMESTAMP');
450 15
                }
451 15
452 11
                try {
453 2
                    $this->default = $this->defaultValue($token2);
454 15
                } catch (Exception $e) {
455
                    throw new RuntimeException("Invalid DEFAULT for '" . $this->name . "'");
456 15
                }
457 15
            } elseif ($token1->eq(Token::IDENTIFIER, 'ON') &&
458
                $stream->consume('UPDATE')
459 1
            ) {
460
                $token2 = $stream->nextToken();
461 14
                if ($token2->eq(Token::IDENTIFIER, 'NOW') ||
462 1
                    $token2->eq(Token::IDENTIFIER, 'CURRENT_TIMESTAMP') ||
463
                    $token2->eq(Token::IDENTIFIER, 'LOCALTIME') ||
464 13
                    $token2->eq(Token::IDENTIFIER, 'LOCALTIMESTAMP')
465
                ) {
466 13
                    if (!$stream->consume([[Token::SYMBOL, '('], [Token::SYMBOL, ')']]) &&
467
                        $token2->eq(Token::IDENTIFIER, 'NOW')
468 25
                    ) {
469
                        throw new RuntimeException("Expected () after keyword NOW");
470 5
                    }
471 1
                    if (!in_array($this->type, ['timestamp', 'datetime'])) {
472
                        throw new RuntimeException("ON UPDATE CURRENT_TIMESTAMP only valid for TIMESTAMP and DATETIME columns");
473 4
                    }
474 4
                    $this->onUpdateCurrentTimestamp = true;
475 22
                } else {
476
                    throw new RuntimeException("Expected CURRENT_TIMESTAMP, NOW, LOCALTIME or LOCALTIMESTAMP");
477 4
                }
478 4
            } elseif ($token1->eq(Token::IDENTIFIER, 'AUTO_INCREMENT')
479 18
            ) {
480 18
                if (!$this->getTypeInfo()->allowAutoIncrement) {
481
                    throw new RuntimeException("AUTO_INCREMENT not allowed for this datatype");
482 12
                }
483 12
                $this->autoIncrement = true;
484 6
                $this->nullable = false;
485
            } elseif ($token1->eq(Token::IDENTIFIER, 'UNIQUE')
486 1
            ) {
487 5
                $stream->consume('KEY');
488 5
                $this->uniqueKey = true;
489
            } elseif (($token1->eq(Token::IDENTIFIER, 'PRIMARY') && $stream->consume('KEY')) ||
490 3
                $token1->eq(Token::IDENTIFIER, 'KEY')
491 1
            ) {
492
                $this->primaryKey = true;
493 2
                $this->nullable = false;
494 2
            } elseif ($token1->eq(Token::IDENTIFIER, 'COMMENT')
495 2
            ) {
496 2
                $this->comment = $stream->expectString();
497 2
            } elseif ($token1->eq(Token::IDENTIFIER, 'SERIAL') &&
498
                $stream->consume('DEFAULT VALUE')
499
            ) {
500 1
                if (!$this->getTypeInfo()->allowAutoIncrement) {
501
                    throw new RuntimeException("SERIAL DEFAULT VALUE is not allowed for this datatype");
502 1
                }
503 1
                $this->uniqueKey = true;
504
                $this->autoIncrement = true;
505
                $this->nullable = false;
506 289
                $this->default = null;
507
            } elseif ($token1->eq(Token::IDENTIFIER, 'DEFAULT_GENERATED')
508
            ) {
509
                // DEFAULT_GENERATED is new in MySQL 8 and not required.
510
                continue;
511
            } else {
512
                $stream->rewind($mark);
513
                break;
514
            }
515
        }
516
    }
517 18
518
    /**
519 18
     * Return the uninitialised value for the column.
520 18
     *
521 18
     * For example, ints will return 0, varchars '', etc. This is independent
522 2
     * of any DEFAULT value specified in the column definition. Null will be
523
     * returned for non-defaultable fields (blobs and texts).
524 16
     *
525 8
     * @return string|null;
526 2
     */
527 2
    public function getUninitialisedValue()
528
    {
529 6
        $typeInfo = $this->getTypeInfo();
530
        switch ($typeInfo->kind) {
531 8
            case 'enum':
532 4
                return $this->elements[0];
533 4
534 1
            case 'int':
535 1
                return '0';
536
537 3
            case 'decimal':
538
                $decimals = is_null($this->decimals) ? 0 : $this->decimals;
0 ignored issues
show
The condition is_null($this->decimals) is always false.
Loading history...
539
                return sprintf("%.{$decimals}f", 0);
540 4
541
            default:
542
                return $typeInfo->uninitialisedValue;
543
        }
544
    }
545
546
    /**
547
     * Get the default value for the given token.
548
     *
549
     * @param Token $token
550
     * @return string|null
551 63
     * @throws Exception
552
     */
553 63
    private function defaultValue(Token $token)
554 10
    {
555 1
        if ($token->eq(Token::IDENTIFIER, 'NULL')) {
556
            if (!$this->nullable) {
557 9
                throw new Exception("Column type cannot have NULL default: $this->type");
558
            }
559
            return null;
560 54
        }
561 15
562 1
        if ($token->eq(Token::IDENTIFIER, 'CURRENT_TIMESTAMP')) {
563
            if (!in_array($this->type, ['timestamp', 'datetime'])) {
564 14
                throw new Exception("Only 'timestamp' and 'datetime' types can have default value of CURRENT_TIMESTAMP");
565
            }
566
            return 'CURRENT_TIMESTAMP';
567 39
        }
568 1
569
        if (!in_array($token->type, [Token::STRING, Token::HEX, Token::BIN, Token::NUMBER])) {
570
            throw new Exception("Invalid token type for default value: $token->type");
571 38
        }
572
573 38
        $typeInfo = $this->getTypeInfo();
574 38
575 3
        switch ($typeInfo->kind) {
576
            case 'bit':
577 35
                return $token->asNumber();
578 4
579 1
            case 'int':
580 1
                return $token->asNumber();
581
                // Comment to appease this phpcs rule:
582 3
                // PSR2.ControlStructures.SwitchDeclaration.TerminatingComment
583
                // There must be a comment when fall-through is intentional
584
                // in a non-empty case body
585
586
            case 'decimal':
587
                $decimals = is_null($this->decimals) ? 0 : $this->decimals;
0 ignored issues
show
The condition is_null($this->decimals) is always false.
Loading history...
588
                return sprintf("%.{$decimals}f", $token->asNumber());
589 31
                // Comment to appease this phpcs rule:
590 3
                // PSR2.ControlStructures.SwitchDeclaration.TerminatingComment
591 3
                // There must be a comment when fall-through is intentional
592 1
                // in a non-empty case body
593 1
594
            case 'date':
595 2
                return $token->asDate();
596
597
            case 'time':
598
                return $token->asTime();
599
600
            case 'datetime':
601
                return $token->asDateTime();
602 28
603 2
            case 'year':
604
                $year = $token->asNumber();
605 26
                if ($token->type !== Token::STRING && $year == 0) {
606 2
                    return '0000';
607
                }
608 24
                if ($year < 70) {
609 5
                    return (string)round($year + 2000);
610
                } elseif ($year <= 99) {
611 19
                    return (string)round($year + 1900);
612 9
                } elseif (1901 <= $year && $year <= 2155) {
613 9
                    return (string)round($year);
614 1
                } else {
615
                    throw new Exception("Invalid default year (1901-2155): $year");
616 8
                }
617 2
                // Comment to appease this phpcs rule:
618 6
                // PSR2.ControlStructures.SwitchDeclaration.TerminatingComment
619 2
                // There must be a comment when fall-through is intentional
620 4
                // in a non-empty case body
621 2
622
            case 'text':
623 2
                return $token->asString();
624
625
            case 'binary':
626
                return str_pad($token->asString(), $this->length, "\0");
627
628
            case 'enum':
629
                if ($token->type !== Token::STRING) {
630 10
                    throw new Exception("Invalid data type for default enum value: $token->type");
631 1
                }
632
                foreach ($this->elements as $element) {
633 9
                    if (strtolower($token->text) === strtolower($element)) {
634 1
                        return $element;
635
                    }
636 8
                }
637 3
                throw new Exception("Default enum value not found in enum: $token->text");
638 1
639
            case 'set':
640 2
                if ($token->type !== Token::STRING) {
641 2
                    throw new Exception("Invalid type for default set value: $token->type");
642 1
                }
643
                if ($token->text === '') {
644
                    return '';
645 1
                }
646
                $defaults = explode(',', strtolower($token->text));
647 5
                foreach ($defaults as $default) {
648 4
                    $match = null;
649 1
                    foreach ($this->elements as $i => $element) {
650
                        if (strtolower($default) === strtolower($element)) {
651 3
                            $match = $i;
652 1
                            break;
653
                        }
654 2
                    }
655 2
                    if (is_null($match)) {
656 2
                        throw new Exception("Default set value not found in set: $token->text");
657 2
                    }
658 2
                    $matches[$match] = $this->elements[$match];
659 1
                }
660 1
                ksort($matches, SORT_NUMERIC);
661
                return implode(',', $matches);
662
663 2
            default:
664 1
                throw new Exception("This kind of data type cannot have a default value: $typeInfo->kind");
665
        }
666 1
    }
667
668 1
    /**
669 1
     * Sets the collation to the specified (table) collation if it was not
670
     * explicitly specified in the column definition. May modify the column's
671
     * type if the charset is binary.
672 1
     *
673
     * @param CollationInfo $tableCollation
674
     * @return void
675
     */
676
    public function applyTableCollation(CollationInfo $tableCollation)
677
    {
678
        if (!$this->collation->isSpecified() &&
679
            $tableCollation->isSpecified()
680
        ) {
681
            $this->collation->setCollation($tableCollation->getCollation());
682
        }
683
684 88
        if ($this->collation->isSpecified() &&
685
            $this->collation->isBinaryCharset()
686 88
        ) {
687 88
            switch ($this->type) {
688
                case 'char':
689 8
                    $this->type = 'binary';
690
                    break;
691
                case 'varchar':
692 88
                    $this->type = 'varbinary';
693 88
                    break;
694
                case 'tinytext':
695 7
                    $this->type = 'tinyblob';
696 7
                    break;
697 1
                case 'text':
698 1
                    $this->type = 'blob';
699 6
                    break;
700 1
                case 'mediumtext':
701 1
                    $this->type = 'mediumblob';
702 5
                    break;
703 1
                case 'longtext':
704 1
                    $this->type = 'longblob';
705 4
                    break;
706 1
                default:
707 1
                    break;
708 3
            }
709 1
        }
710 1
    }
711 2
712 1
    /**
713 1
     * Returns the column definition as an SQL fragment, relative to the
714
     * specified table collation.
715 1
     *
716
     * @param CollationInfo $tableCollation
717
     * @return string
718 88
     */
719
    public function toString(CollationInfo $tableCollation)
720
    {
721
        $text = Token::escapeIdentifier($this->name) . " " . $this->type;
722
        $typeInfo = $this->getTypeInfo();
723
724
        if ($this->length !== null) {
725
            $text .= "(" . $this->length;
726
            if ($typeInfo->kind === 'decimal') {
727 254
                $text .= "," . $this->decimals;
728
            }
729 254
            $text .= ")";
730 254
        }
731
732 254
        if (count($this->elements) > 0) {
733 162
            $text .= "(";
734 162
            $text .= implode(',', array_map('Graze\Morphism\Parse\Token::escapeString', $this->elements));
735 24
            $text .= ")";
736
        }
737 162
738
        if ($this->unsigned) {
739
            $text .= " unsigned";
740 254
        }
741 9
742 9
        if ($typeInfo->allowCharset) {
743 9
            $collation = $this->collation;
744
            if ($collation->isSpecified()) {
745
                if (!$tableCollation->isSpecified() ||
746 254
                    $tableCollation->getCollation() !== $collation->getCollation()
747 17
                ) {
748
                    $text .= " CHARACTER SET " . $collation->getCharset();
749
                }
750 254
                if (!$collation->isDefaultCollation()) {
751 6
                    $text .= " COLLATE " . $collation->getCollation();
752
                }
753
            }
754 254
        }
755 41
756 41
        if ($this->nullable) {
757 2
            if ($this->type === 'timestamp') {
758 2
                $text .= " NULL";
759
            }
760 2
        } else {
761
            $text .= " NOT NULL";
762 2
        }
763 1
764
        if ($this->autoIncrement) {
765
            $text .= " AUTO_INCREMENT";
766
        }
767
768 254
        if (is_null($this->default)) {
0 ignored issues
show
The condition is_null($this->default) is always false.
Loading history...
769 190
            if ($this->nullable && $typeInfo->allowDefault) {
770 190
                $text .= " DEFAULT NULL";
771
            }
772
        } elseif (in_array($this->type, ['timestamp', 'datetime']) &&
773 72
            $this->default === 'CURRENT_TIMESTAMP'
774
        ) {
775
            $text .= " DEFAULT CURRENT_TIMESTAMP";
776 254
        } elseif ($this->type === 'bit') {
777 4
            $text .= " DEFAULT b'" . decbin((int)$this->default) . "'";
778
        } else {
779
            $text .= " DEFAULT " . Token::escapeString($this->default);
780 254
        }
781 197
782 197
        if ($this->onUpdateCurrentTimestamp) {
783
            $text .= " ON UPDATE CURRENT_TIMESTAMP";
784 60
        }
785 60
786
        if (!is_null($this->comment)) {
787 17
            $text .= " COMMENT " . Token::escapeString($this->comment);
788 45
        }
789 4
790
        return $text;
791 41
    }
792
}
793