GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Pull Request — master (#49)
by Burhan
02:28
created

ColumnDefinition::parseColumnOptions()   D

Complexity

Conditions 34
Paths 22

Size

Total Lines 94
Code Lines 66

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 64
CRAP Score 34

Importance

Changes 0
Metric Value
cc 34
eloc 66
nc 22
nop 1
dl 0
loc 94
rs 4.1666
c 0
b 0
f 0
ccs 64
cts 64
cp 1
crap 34

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

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
723 155
            $text .= "(" . $this->length;
724 155
            if ($typeInfo->kind === 'decimal') {
725 24
                $text .= "," . $this->decimals;
726
            }
727 155
            $text .= ")";
728
        }
729
730 243
        if (count($this->elements) > 0) {
731 9
            $text .= "(";
732 9
            $text .= implode(',', array_map('Graze\Morphism\Parse\Token::escapeString', $this->elements));
733 9
            $text .= ")";
734
        }
735
736 243
        if ($this->unsigned) {
737 17
            $text .= " unsigned";
738
        }
739
740 243
        if ($this->zerofill) {
741 6
            $text .= " zerofill";
742
        }
743
744 243
        if ($typeInfo->allowCharset) {
745 41
            $collation = $this->collation;
746 41
            if ($collation->isSpecified()) {
747 2
                if (!$tableCollation->isSpecified() ||
748 2
                    $tableCollation->getCollation() !== $collation->getCollation()
749
                ) {
750 2
                    $text .= " CHARACTER SET " . $collation->getCharset();
751
                }
752 2
                if (!$collation->isDefaultCollation()) {
753 1
                    $text .= " COLLATE " . $collation->getCollation();
754
                }
755
            }
756
        }
757
758 243
        if ($this->nullable) {
759 180
            if ($this->type === 'timestamp') {
760 180
                $text .= " NULL";
761
            }
762
        } else {
763 71
            $text .= " NOT NULL";
764
        }
765
766 243
        if ($this->autoIncrement) {
767 4
            $text .= " AUTO_INCREMENT";
768
        }
769
770 243
        if (is_null($this->default)) {
771 187
            if ($this->nullable && $typeInfo->allowDefault) {
772 187
                $text .= " DEFAULT NULL";
773
            }
774 59
        } elseif (in_array($this->type, ['timestamp', 'datetime']) &&
775 59
            $this->default === 'CURRENT_TIMESTAMP'
776
        ) {
777 16
            $text .= " DEFAULT CURRENT_TIMESTAMP";
778 45
        } elseif ($this->type === 'bit') {
779 4
            $text .= " DEFAULT b'" . decbin($this->default) . "'";
0 ignored issues
show
Bug introduced by
$this->default of type string is incompatible with the type integer expected by parameter $number of decbin(). ( Ignorable by Annotation )

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

779
            $text .= " DEFAULT b'" . decbin(/** @scrutinizer ignore-type */ $this->default) . "'";
Loading history...
780
        } else {
781 41
            $text .= " DEFAULT " . Token::escapeString($this->default);
782
        }
783
784 243
        if ($this->onUpdateCurrentTimestamp) {
785 15
            $text .= " ON UPDATE CURRENT_TIMESTAMP";
786
        }
787
788 243
        if (!is_null($this->comment)) {
789 1
            $text .= " COMMENT " . Token::escapeString($this->comment);
790
        }
791
792 243
        return $text;
793
    }
794
}
795