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 (#63)
by Brendan
03:15
created

ColumnDefinition::parseColumnOptions()   D

Complexity

Conditions 35
Paths 23

Size

Total Lines 98
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 65
CRAP Score 35.0043

Importance

Changes 0
Metric Value
cc 35
eloc 68
c 0
b 0
f 0
nc 23
nop 1
dl 0
loc 98
ccs 65
cts 66
cp 0.9848
crap 35.0043
rs 4.1666

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