GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( d274d7...b4c702 )
by
unknown
10:03 queued 17s
created

ColumnDefinition   F

Complexity

Total Complexity 171

Size/Duplication

Total Lines 793
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 472
dl 0
loc 793
rs 2
c 2
b 0
f 0
ccs 405
cts 405
cp 1
wmc 171

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getTypeInfo() 0 26 3
D defaultValue() 0 122 35
F parseColumnDatatype() 0 207 53
D parseColumnOptions() 0 98 35
A addIndex() 0 11 1
A parse() 0 11 3
B getUninitialisedValue() 0 24 7
A __construct() 0 3 1
B applyTableCollation() 0 32 11
F toString() 0 76 22

How to fix   Complexity   

Complex Class

Complex classes like ColumnDefinition often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ColumnDefinition, and based on these observations, apply Extract Interface, too.

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