Passed
Branch master (6c65a4)
by Christian
16:31
created

Parser::createForeignKeyDefinitionItem()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 15
nc 2
nop 0
dl 0
loc 26
rs 8.8571
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace TYPO3\CMS\Core\Database\Schema\Parser;
5
6
/*
7
 * This file is part of the TYPO3 CMS project.
8
 *
9
 * It is free software; you can redistribute it and/or modify it under
10
 * the terms of the GNU General Public License, either version 2
11
 * of the License, or any later version.
12
 *
13
 * For the full copyright and license information, please read the
14
 * LICENSE.txt file that was distributed with this source code.
15
 *
16
 * The TYPO3 project - inspiring people to share!
17
 */
18
19
use Doctrine\DBAL\Schema\Table;
20
use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
21
use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement;
22
23
/**
24
 * An LL(*) recursive-descent parser for MySQL CREATE TABLE statements.
25
 * Parses a CREATE TABLE statement, reports any errors in it, and generates an AST.
26
 */
27
class Parser
28
{
29
    /**
30
     * The lexer.
31
     *
32
     * @var Lexer
33
     */
34
    protected $lexer;
35
36
    /**
37
     * The statement to parse.
38
     *
39
     * @var string
40
     */
41
    protected $statement;
42
43
    /**
44
     * Creates a new statement parser object.
45
     *
46
     * @param string $statement The statement to parse.
47
     */
48
    public function __construct(string $statement)
49
    {
50
        $this->statement = $statement;
51
        $this->lexer = new Lexer($statement);
52
    }
53
54
    /**
55
     * Gets the lexer used by the parser.
56
     *
57
     * @return Lexer
58
     */
59
    public function getLexer(): Lexer
60
    {
61
        return $this->lexer;
62
    }
63
64
    /**
65
     * Parses and builds AST for the given Query.
66
     *
67
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\AbstractCreateStatement
68
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
69
     */
70
    public function getAST(): AST\AbstractCreateStatement
71
    {
72
        // Parse & build AST
73
        return $this->queryLanguage();
74
    }
75
76
    /**
77
     * Attempts to match the given token with the current lookahead token.
78
     *
79
     * If they match, updates the lookahead token; otherwise raises a syntax
80
     * error.
81
     *
82
     * @param int $token The token type.
83
     *
84
     *
85
     * @throws StatementException If the tokens don't match.
86
     */
87
    public function match($token)
88
    {
89
        $lookaheadType = $this->lexer->lookahead['type'];
90
91
        // Short-circuit on first condition, usually types match
92
        if ($lookaheadType !== $token) {
93
            // If parameter is not identifier (1-99) must be exact match
94
            if ($token < Lexer::T_IDENTIFIER) {
95
                $this->syntaxError($this->lexer->getLiteral($token));
96
            }
97
98
            // If parameter is keyword (200+) must be exact match
99
            if ($token > Lexer::T_IDENTIFIER) {
100
                $this->syntaxError($this->lexer->getLiteral($token));
101
            }
102
103
            // If parameter is MATCH then FULL, PARTIAL or SIMPLE must follow
104
            if ($token === Lexer::T_MATCH
105
                && $lookaheadType !== Lexer::T_FULL
106
                && $lookaheadType !== Lexer::T_PARTIAL
107
                && $lookaheadType !== Lexer::T_SIMPLE
108
            ) {
109
                $this->syntaxError($this->lexer->getLiteral($token));
110
            }
111
112
            if ($token === Lexer::T_ON && $lookaheadType !== Lexer::T_DELETE && $lookaheadType !== Lexer::T_UPDATE) {
113
                $this->syntaxError($this->lexer->getLiteral($token));
114
            }
115
        }
116
117
        $this->lexer->moveNext();
118
    }
119
120
    /**
121
     * Frees this parser, enabling it to be reused.
122
     *
123
     * @param bool $deep Whether to clean peek and reset errors.
124
     * @param int $position Position to reset.
125
     */
126
    public function free($deep = false, $position = 0)
127
    {
128
        // WARNING! Use this method with care. It resets the scanner!
129
        $this->lexer->resetPosition($position);
130
131
        // Deep = true cleans peek and also any previously defined errors
132
        if ($deep) {
133
            $this->lexer->resetPeek();
134
        }
135
136
        $this->lexer->token = null;
137
        $this->lexer->lookahead = null;
138
    }
139
140
    /**
141
     * Parses a statement string.
142
     *
143
     * @return Table[]
144
     * @throws \Doctrine\DBAL\Schema\SchemaException
145
     * @throws \RuntimeException
146
     * @throws \InvalidArgumentException
147
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
148
     */
149
    public function parse(): array
150
    {
151
        $ast = $this->getAST();
152
153
        if (!$ast instanceof CreateTableStatement) {
154
            return [];
155
        }
156
157
        $tableBuilder = new TableBuilder();
158
        $table = $tableBuilder->create($ast);
159
160
        return [$table];
161
    }
162
163
    /**
164
     * Generates a new syntax error.
165
     *
166
     * @param string $expected Expected string.
167
     * @param array|null $token Got token.
168
     *
169
     *
170
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
171
     */
172
    public function syntaxError($expected = '', $token = null)
173
    {
174
        if ($token === null) {
175
            $token = $this->lexer->lookahead;
176
        }
177
178
        $tokenPos = $token['position'] ?? '-1';
179
180
        $message = "line 0, col {$tokenPos}: Error: ";
181
        $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected ';
182
        $message .= ($this->lexer->lookahead === null) ? 'end of string.' : "'{$token['value']}'";
183
184
        throw StatementException::syntaxError($message, StatementException::sqlError($this->statement));
185
    }
186
187
    /**
188
     * Generates a new semantical error.
189
     *
190
     * @param string $message Optional message.
191
     * @param array|null $token Optional token.
192
     *
193
     *
194
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
195
     */
196
    public function semanticalError($message = '', $token = null)
197
    {
198
        if ($token === null) {
199
            $token = $this->lexer->lookahead;
200
        }
201
202
        // Minimum exposed chars ahead of token
203
        $distance = 12;
204
205
        // Find a position of a final word to display in error string
206
        $createTableStatement = $this->statement;
207
        $length = strlen($createTableStatement);
208
        $pos = $token['position'] + $distance;
209
        $pos = strpos($createTableStatement, ' ', ($length > $pos) ? $pos : $length);
210
        $length = ($pos !== false) ? $pos - $token['position'] : $distance;
211
212
        $tokenPos = array_key_exists('position', $token) && $token['position'] > 0 ? $token['position'] : '-1';
213
        $tokenStr = substr($createTableStatement, $token['position'], $length);
214
215
        // Building informative message
216
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
217
218
        throw StatementException::semanticalError($message, StatementException::sqlError($this->statement));
219
    }
220
221
    /**
222
     * Peeks beyond the matched closing parenthesis and returns the first token after that one.
223
     *
224
     * @param bool $resetPeek Reset peek after finding the closing parenthesis.
225
     *
226
     * @return array
227
     */
228
    protected function peekBeyondClosingParenthesis($resetPeek = true)
229
    {
230
        $token = $this->lexer->peek();
231
        $numUnmatched = 1;
232
233
        while ($numUnmatched > 0 && $token !== null) {
234
            switch ($token['type']) {
235
                case Lexer::T_OPEN_PARENTHESIS:
236
                    ++$numUnmatched;
237
                    break;
238
                case Lexer::T_CLOSE_PARENTHESIS:
239
                    --$numUnmatched;
240
                    break;
241
                default:
242
                    // Do nothing
243
            }
244
245
            $token = $this->lexer->peek();
246
        }
247
248
        if ($resetPeek) {
249
            $this->lexer->resetPeek();
250
        }
251
252
        return $token;
253
    }
254
255
    /**
256
     * queryLanguage ::= CreateTableStatement
257
     *
258
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\AbstractCreateStatement
259
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
260
     */
261
    public function queryLanguage(): AST\AbstractCreateStatement
262
    {
263
        $this->lexer->moveNext();
264
265
        if ($this->lexer->lookahead['type'] !== Lexer::T_CREATE) {
266
            $this->syntaxError('CREATE');
267
        }
268
269
        $statement = $this->createStatement();
270
271
        // Check for end of string
272
        if ($this->lexer->lookahead !== null) {
273
            $this->syntaxError('end of string');
274
        }
275
276
        return $statement;
277
    }
278
279
    /**
280
     * CreateStatement ::= CREATE [TEMPORARY] TABLE
281
     * Abstraction to allow for support of other schema objects like views in the future.
282
     *
283
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\AbstractCreateStatement
284
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
285
     */
286
    public function createStatement(): AST\AbstractCreateStatement
287
    {
288
        $statement = null;
289
        $this->match(Lexer::T_CREATE);
290
291
        switch ($this->lexer->lookahead['type']) {
292
            case Lexer::T_TEMPORARY:
293
                // Intentional fall-through
294
            case Lexer::T_TABLE:
295
                $statement = $this->createTableStatement();
296
                break;
297
            default:
298
                $this->syntaxError('TEMPORARY or TABLE');
299
                break;
300
        }
301
302
        $this->match(Lexer::T_SEMICOLON);
303
304
        return $statement;
305
    }
306
307
    /**
308
     * CreateTableStatement ::= CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name (create_definition,...) [tbl_options]
309
     *
310
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
311
     */
312
    protected function createTableStatement(): AST\CreateTableStatement
313
    {
314
        $createTableStatement = new AST\CreateTableStatement($this->createTableClause(), $this->createDefinition());
315
316
        if (!$this->lexer->isNextToken(Lexer::T_SEMICOLON)) {
317
            $createTableStatement->tableOptions = $this->tableOptions();
318
        }
319
        return $createTableStatement;
320
    }
321
322
    /**
323
     * CreateTableClause ::= CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name
324
     *
325
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableClause
326
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
327
     */
328
    protected function createTableClause(): AST\CreateTableClause
329
    {
330
        $isTemporary = false;
331
        // Check for TEMPORARY
332
        if ($this->lexer->isNextToken(Lexer::T_TEMPORARY)) {
333
            $this->match(Lexer::T_TEMPORARY);
334
            $isTemporary = true;
335
        }
336
337
        $this->match(Lexer::T_TABLE);
338
339
        // Check for IF NOT EXISTS
340
        if ($this->lexer->isNextToken(Lexer::T_IF)) {
341
            $this->match(Lexer::T_IF);
342
            $this->match(Lexer::T_NOT);
343
            $this->match(Lexer::T_EXISTS);
344
        }
345
346
        // Process schema object name (table name)
347
        $tableName = $this->schemaObjectName();
348
349
        return new AST\CreateTableClause($tableName, $isTemporary);
350
    }
351
352
    /**
353
     * Parses the table field/index definition
354
     *
355
     * createDefinition ::= (
356
     *  col_name column_definition
357
     *  | [CONSTRAINT [symbol]] PRIMARY KEY [index_type] (index_col_name,...) [index_option] ...
358
     *  | {INDEX|KEY} [index_name] [index_type] (index_col_name,...) [index_option] ...
359
     *  | [CONSTRAINT [symbol]] UNIQUE [INDEX|KEY] [index_name] [index_type] (index_col_name,...) [index_option] ...
360
     *  | {FULLTEXT|SPATIAL} [INDEX|KEY] [index_name] (index_col_name,...) [index_option] ...
361
     *  | [CONSTRAINT [symbol]] FOREIGN KEY [index_name] (index_col_name,...) reference_definition
362
     *  | CHECK (expr)
363
     * )
364
     *
365
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateDefinition
366
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
367
     */
368
    protected function createDefinition(): AST\CreateDefinition
369
    {
370
        $createDefinitions = [];
371
372
        // Process opening parenthesis
373
        $this->match(Lexer::T_OPEN_PARENTHESIS);
374
375
        $createDefinitions[] = $this->createDefinitionItem();
376
377
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
378
            $this->match(Lexer::T_COMMA);
379
380
            // TYPO3 previously accepted invalid SQL files where a create definition
381
            // item terminated with a comma before the final closing parenthesis.
382
            // Silently swallow the extra comma and stop the create definition parsing.
383
            if ($this->lexer->isNextToken(Lexer::T_CLOSE_PARENTHESIS)) {
384
                break;
385
            }
386
387
            $createDefinitions[] = $this->createDefinitionItem();
388
        }
389
390
        // Process closing parenthesis
391
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
392
393
        return new AST\CreateDefinition($createDefinitions);
394
    }
395
396
    /**
397
     * Parse the definition of a single column or index
398
     *
399
     * @see createDefinition()
400
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\AbstractCreateDefinitionItem
401
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
402
     */
403
    protected function createDefinitionItem(): AST\AbstractCreateDefinitionItem
404
    {
405
        $definitionItem = null;
406
407
        switch ($this->lexer->lookahead['type']) {
408
            case Lexer::T_FULLTEXT:
409
                // Intentional fall-through
410
            case Lexer::T_SPATIAL:
411
                // Intentional fall-through
412
            case Lexer::T_PRIMARY:
413
                // Intentional fall-through
414
            case Lexer::T_UNIQUE:
415
                // Intentional fall-through
416
            case Lexer::T_KEY:
417
                // Intentional fall-through
418
            case Lexer::T_INDEX:
419
                $definitionItem = $this->createIndexDefinitionItem();
420
                break;
421
            case Lexer::T_FOREIGN:
422
                $definitionItem = $this->createForeignKeyDefinitionItem();
423
                break;
424
            case Lexer::T_CONSTRAINT:
425
                $this->semanticalError('CONSTRAINT [symbol] index definition part not supported');
426
                break;
427
            case Lexer::T_CHECK:
428
                $this->semanticalError('CHECK (expr) create definition not supported');
429
                break;
430
            default:
431
                $definitionItem = $this->createColumnDefinitionItem();
432
        }
433
434
        return $definitionItem;
435
    }
436
437
    /**
438
     * Parses an index definition item contained in the create definition
439
     *
440
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateIndexDefinitionItem
441
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
442
     */
443
    protected function createIndexDefinitionItem(): AST\CreateIndexDefinitionItem
444
    {
445
        $indexName = null;
446
        $isPrimary = false;
447
        $isFulltext = false;
448
        $isSpatial = false;
449
        $isUnique = false;
450
        $indexDefinition = new AST\CreateIndexDefinitionItem();
451
452
        switch ($this->lexer->lookahead['type']) {
453
            case Lexer::T_PRIMARY:
454
                $this->match(Lexer::T_PRIMARY);
455
                // KEY is a required keyword for PRIMARY index
456
                $this->match(Lexer::T_KEY);
457
                $isPrimary = true;
458
                break;
459
            case Lexer::T_KEY:
460
                // Plain index, no special configuration
461
                $this->match(Lexer::T_KEY);
462
                break;
463
            case Lexer::T_INDEX:
464
                // Plain index, no special configuration
465
                $this->match(Lexer::T_INDEX);
466
                break;
467
            case Lexer::T_UNIQUE:
468
                $this->match(Lexer::T_UNIQUE);
469
                // INDEX|KEY are optional keywords for UNIQUE index
470
                if ($this->lexer->isNextTokenAny([Lexer::T_INDEX, Lexer::T_KEY])) {
471
                    $this->lexer->moveNext();
472
                }
473
                $isUnique = true;
474
                break;
475
            case Lexer::T_FULLTEXT:
476
                $this->match(Lexer::T_FULLTEXT);
477
                // INDEX|KEY are optional keywords for FULLTEXT index
478
                if ($this->lexer->isNextTokenAny([Lexer::T_INDEX, Lexer::T_KEY])) {
479
                    $this->lexer->moveNext();
480
                }
481
                $isFulltext = true;
482
                break;
483
            case Lexer::T_SPATIAL:
484
                $this->match(Lexer::T_SPATIAL);
485
                // INDEX|KEY are optional keywords for SPATIAL index
486
                if ($this->lexer->isNextTokenAny([Lexer::T_INDEX, Lexer::T_KEY])) {
487
                    $this->lexer->moveNext();
488
                }
489
                $isSpatial = true;
490
                break;
491
            default:
492
                $this->syntaxError('PRIMARY, KEY, INDEX, UNIQUE, FULLTEXT or SPATIAL');
493
        }
494
495
        // PRIMARY KEY has no name in MySQL
496
        if (!$indexDefinition->isPrimary) {
497
            $indexName = $this->indexName();
498
        }
499
500
        $indexDefinition = new AST\CreateIndexDefinitionItem(
501
            $indexName,
502
            $isPrimary,
503
            $isUnique,
504
            $isSpatial,
505
            $isFulltext
506
        );
507
508
        // FULLTEXT and SPATIAL indexes can not have a type definition
509
        if (!$isFulltext && !$isSpatial) {
510
            $indexDefinition->indexType = $this->indexType();
511
        }
512
513
        $this->match(Lexer::T_OPEN_PARENTHESIS);
514
515
        $indexDefinition->columnNames[] = $this->indexColumnName();
516
517
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
518
            $this->match(Lexer::T_COMMA);
519
            $indexDefinition->columnNames[] = $this->indexColumnName();
520
        }
521
522
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
523
524
        $indexDefinition->options = $this->indexOptions();
525
526
        return $indexDefinition;
527
    }
528
529
    /**
530
     * Parses an foreign key definition item contained in the create definition
531
     *
532
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateForeignKeyDefinitionItem
533
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
534
     */
535
    protected function createForeignKeyDefinitionItem(): AST\CreateForeignKeyDefinitionItem
536
    {
537
        $this->match(Lexer::T_FOREIGN);
538
        $this->match(Lexer::T_KEY);
539
540
        $indexName = $this->indexName();
541
542
        $this->match(Lexer::T_OPEN_PARENTHESIS);
543
544
        $indexColumns = [];
545
        $indexColumns[] = $this->indexColumnName();
546
547
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
548
            $this->match(Lexer::T_COMMA);
549
            $indexColumns[] = $this->indexColumnName();
550
        }
551
552
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
553
554
        $foreignKeyDefinition = new AST\CreateForeignKeyDefinitionItem(
555
            $indexName,
556
            $indexColumns,
557
            $this->referenceDefinition()
558
        );
559
560
        return $foreignKeyDefinition;
561
    }
562
563
    /**
564
     * Return the name of an index. No name has been supplied if the next token is USING
565
     * which defines the index type.
566
     *
567
     * @return AST\Identifier
568
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
569
     */
570
    public function indexName(): AST\Identifier
571
    {
572
        $indexName = new AST\Identifier(null);
573
        if (!$this->lexer->isNextTokenAny([Lexer::T_USING, Lexer::T_OPEN_PARENTHESIS])) {
574
            $indexName = $this->schemaObjectName();
575
        }
576
577
        return $indexName;
578
    }
579
580
    /**
581
     * IndexType ::= USING { BTREE | HASH }
582
     *
583
     * @return string
584
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
585
     */
586
    public function indexType(): string
587
    {
588
        $indexType = '';
589
        if (!$this->lexer->isNextToken(Lexer::T_USING)) {
590
            return $indexType;
591
        }
592
593
        $this->match(Lexer::T_USING);
594
595
        switch ($this->lexer->lookahead['type']) {
596
            case Lexer::T_BTREE:
597
                $this->match(Lexer::T_BTREE);
598
                $indexType = 'BTREE';
599
                break;
600
            case Lexer::T_HASH:
601
                $this->match(Lexer::T_HASH);
602
                $indexType = 'HASH';
603
                break;
604
            default:
605
                $this->syntaxError('BTREE or HASH');
606
        }
607
608
        return $indexType;
609
    }
610
611
    /**
612
     * IndexOptions ::=  KEY_BLOCK_SIZE [=] value
613
     *  | index_type
614
     *  | WITH PARSER parser_name
615
     *  | COMMENT 'string'
616
     *
617
     * @return array
618
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
619
     */
620
    public function indexOptions(): array
621
    {
622
        $options = [];
623
624
        while ($this->lexer->lookahead && !$this->lexer->isNextTokenAny([Lexer::T_COMMA, Lexer::T_CLOSE_PARENTHESIS])) {
625
            switch ($this->lexer->lookahead['type']) {
626
                case Lexer::T_KEY_BLOCK_SIZE:
627
                    $this->match(Lexer::T_KEY_BLOCK_SIZE);
628
                    if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
629
                        $this->match(Lexer::T_EQUALS);
630
                    }
631
                    $this->lexer->moveNext();
632
                    $options['key_block_size'] = (int)$this->lexer->token['value'];
633
                    break;
634
                case Lexer::T_USING:
635
                    $options['index_type'] = $this->indexType();
636
                    break;
637
                case Lexer::T_WITH:
638
                    $this->match(Lexer::T_WITH);
639
                    $this->match(Lexer::T_PARSER);
640
                    $options['parser'] = $this->schemaObjectName();
641
                    break;
642
                case Lexer::T_COMMENT:
643
                    $this->match(Lexer::T_COMMENT);
644
                    $this->match(Lexer::T_STRING);
645
                    $options['comment'] = $this->lexer->token['value'];
646
                    break;
647
                default:
648
                    $this->syntaxError('KEY_BLOCK_SIZE, USING, WITH PARSER or COMMENT');
649
            }
650
        }
651
652
        return $options;
653
    }
654
655
    /**
656
     * CreateColumnDefinitionItem ::= col_name column_definition
657
     *
658
     * column_definition:
659
     *   data_type [NOT NULL | NULL] [DEFAULT default_value]
660
     *     [AUTO_INCREMENT] [UNIQUE [KEY] | [PRIMARY] KEY]
661
     *     [COMMENT 'string']
662
     *     [COLUMN_FORMAT {FIXED|DYNAMIC|DEFAULT}]
663
     *     [STORAGE {DISK|MEMORY|DEFAULT}]
664
     *     [reference_definition]
665
     *
666
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem
667
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
668
     */
669
    protected function createColumnDefinitionItem(): AST\CreateColumnDefinitionItem
670
    {
671
        $columnName = $this->schemaObjectName();
672
        $dataType = $this->columnDataType();
673
674
        $columnDefinitionItem = new AST\CreateColumnDefinitionItem($columnName, $dataType);
675
676
        while ($this->lexer->lookahead && !$this->lexer->isNextTokenAny([Lexer::T_COMMA, Lexer::T_CLOSE_PARENTHESIS])) {
677
            switch ($this->lexer->lookahead['type']) {
678
                case Lexer::T_NOT:
679
                    $columnDefinitionItem->allowNull = false;
680
                    $this->match(Lexer::T_NOT);
681
                    $this->match(Lexer::T_NULL);
682
                    break;
683
                case Lexer::T_NULL:
684
                    $columnDefinitionItem->null = true;
0 ignored issues
show
Bug introduced by
The property null does not seem to exist on TYPO3\CMS\Core\Database\...ateColumnDefinitionItem.
Loading history...
685
                    $this->match(Lexer::T_NULL);
686
                    break;
687
                case Lexer::T_DEFAULT:
688
                    $columnDefinitionItem->hasDefaultValue = true;
689
                    $columnDefinitionItem->defaultValue = $this->columnDefaultValue();
690
                    break;
691
                case Lexer::T_AUTO_INCREMENT:
692
                    $columnDefinitionItem->autoIncrement = true;
693
                    $this->match(Lexer::T_AUTO_INCREMENT);
694
                    break;
695
                case Lexer::T_UNIQUE:
696
                    $columnDefinitionItem->unique = true;
697
                    $this->match(Lexer::T_UNIQUE);
698
                    if ($this->lexer->isNextToken(Lexer::T_KEY)) {
699
                        $this->match(Lexer::T_KEY);
700
                    }
701
                    break;
702
                case Lexer::T_PRIMARY:
703
                    $columnDefinitionItem->primary = true;
704
                    $this->match(Lexer::T_PRIMARY);
705
                    if ($this->lexer->isNextToken(Lexer::T_KEY)) {
706
                        $this->match(Lexer::T_KEY);
707
                    }
708
                    break;
709
                case Lexer::T_KEY:
710
                    $columnDefinitionItem->index = true;
711
                    $this->match(Lexer::T_KEY);
712
                    break;
713
                case Lexer::T_COMMENT:
714
                    $this->match(Lexer::T_COMMENT);
715
                    if ($this->lexer->isNextToken(Lexer::T_STRING)) {
716
                        $columnDefinitionItem->comment = $this->lexer->lookahead['value'];
717
                        $this->match(Lexer::T_STRING);
718
                    }
719
                    break;
720
                case Lexer::T_COLUMN_FORMAT:
721
                    $this->match(Lexer::T_COLUMN_FORMAT);
722
                    if ($this->lexer->isNextToken(Lexer::T_FIXED)) {
723
                        $columnDefinitionItem->columnFormat = 'fixed';
724
                        $this->match(Lexer::T_FIXED);
725
                    } elseif ($this->lexer->isNextToken(Lexer::T_DYNAMIC)) {
726
                        $columnDefinitionItem->columnFormat = 'dynamic';
727
                        $this->match(Lexer::T_DYNAMIC);
728
                    } else {
729
                        $this->match(Lexer::T_DEFAULT);
730
                    }
731
                    break;
732
                case Lexer::T_STORAGE:
733
                    $this->match(Lexer::T_STORAGE);
734
                    if ($this->lexer->isNextToken(Lexer::T_MEMORY)) {
735
                        $columnDefinitionItem->storage = 'memory';
736
                        $this->match(Lexer::T_MEMORY);
737
                    } elseif ($this->lexer->isNextToken(Lexer::T_DISK)) {
738
                        $columnDefinitionItem->storage = 'disk';
739
                        $this->match(Lexer::T_DISK);
740
                    } else {
741
                        $this->match(Lexer::T_DEFAULT);
742
                    }
743
                    break;
744
                case Lexer::T_REFERENCES:
745
                    $columnDefinitionItem->reference = $this->referenceDefinition();
746
                    break;
747
                default:
748
                    $this->syntaxError(
749
                        'NOT, NULL, DEFAULT, AUTO_INCREMENT, UNIQUE, ' .
750
                        'PRIMARY, COMMENT, COLUMN_FORMAT, STORAGE or REFERENCES'
751
                    );
752
            }
753
        }
754
755
        return $columnDefinitionItem;
756
    }
757
758
    /**
759
     * DataType ::= BIT[(length)]
760
     *   | TINYINT[(length)] [UNSIGNED] [ZEROFILL]
761
     *   | SMALLINT[(length)] [UNSIGNED] [ZEROFILL]
762
     *   | MEDIUMINT[(length)] [UNSIGNED] [ZEROFILL]
763
     *   | INT[(length)] [UNSIGNED] [ZEROFILL]
764
     *   | INTEGER[(length)] [UNSIGNED] [ZEROFILL]
765
     *   | BIGINT[(length)] [UNSIGNED] [ZEROFILL]
766
     *   | REAL[(length,decimals)] [UNSIGNED] [ZEROFILL]
767
     *   | DOUBLE[(length,decimals)] [UNSIGNED] [ZEROFILL]
768
     *   | FLOAT[(length,decimals)] [UNSIGNED] [ZEROFILL]
769
     *   | DECIMAL[(length[,decimals])] [UNSIGNED] [ZEROFILL]
770
     *   | NUMERIC[(length[,decimals])] [UNSIGNED] [ZEROFILL]
771
     *   | DATE
772
     *   | TIME[(fsp)]
773
     *   | TIMESTAMP[(fsp)]
774
     *   | DATETIME[(fsp)]
775
     *   | YEAR
776
     *   | CHAR[(length)] [BINARY] [CHARACTER SET charset_name] [COLLATE collation_name]
777
     *   | VARCHAR(length) [BINARY] [CHARACTER SET charset_name] [COLLATE collation_name]
778
     *   | BINARY[(length)]
779
     *   | VARBINARY(length)
780
     *   | TINYBLOB
781
     *   | BLOB
782
     *   | MEDIUMBLOB
783
     *   | LONGBLOB
784
     *   | TINYTEXT [BINARY] [CHARACTER SET charset_name] [COLLATE collation_name]
785
     *   | TEXT [BINARY] [CHARACTER SET charset_name] [COLLATE collation_name]
786
     *   | MEDIUMTEXT [BINARY] [CHARACTER SET charset_name] [COLLATE collation_name]
787
     *   | LONGTEXT [BINARY] [CHARACTER SET charset_name] [COLLATE collation_name]
788
     *   | ENUM(value1,value2,value3,...) [CHARACTER SET charset_name] [COLLATE collation_name]
789
     *   | SET(value1,value2,value3,...) [CHARACTER SET charset_name] [COLLATE collation_name]
790
     *   | JSON
791
     *
792
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\AbstractDataType
793
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
794
     */
795
    protected function columnDataType(): AST\DataType\AbstractDataType
796
    {
797
        $dataType = null;
798
799
        switch ($this->lexer->lookahead['type']) {
800
            case Lexer::T_BIT:
801
                $this->match(Lexer::T_BIT);
802
                $dataType = new AST\DataType\BitDataType(
803
                    $this->dataTypeLength()
804
                );
805
                break;
806
            case Lexer::T_TINYINT:
807
                $this->match(Lexer::T_TINYINT);
808
                $dataType = new AST\DataType\TinyIntDataType(
809
                    $this->dataTypeLength(),
810
                    $this->numericDataTypeOptions()
811
                );
812
                break;
813
            case Lexer::T_SMALLINT:
814
                $this->match(Lexer::T_SMALLINT);
815
                $dataType = new AST\DataType\SmallIntDataType(
816
                    $this->dataTypeLength(),
817
                    $this->numericDataTypeOptions()
818
                );
819
                break;
820
            case Lexer::T_MEDIUMINT:
821
                $this->match(Lexer::T_MEDIUMINT);
822
                $dataType = new AST\DataType\MediumIntDataType(
823
                    $this->dataTypeLength(),
824
                    $this->numericDataTypeOptions()
825
                );
826
                break;
827
            case Lexer::T_INT:
828
                $this->match(Lexer::T_INT);
829
                $dataType = new AST\DataType\IntegerDataType(
830
                    $this->dataTypeLength(),
831
                    $this->numericDataTypeOptions()
832
                );
833
                break;
834
            case Lexer::T_INTEGER:
835
                $this->match(Lexer::T_INTEGER);
836
                $dataType = new AST\DataType\IntegerDataType(
837
                    $this->dataTypeLength(),
838
                    $this->numericDataTypeOptions()
839
                );
840
                break;
841
            case Lexer::T_BIGINT:
842
                $this->match(Lexer::T_BIGINT);
843
                $dataType = new AST\DataType\BigIntDataType(
844
                    $this->dataTypeLength(),
845
                    $this->numericDataTypeOptions()
846
                );
847
                break;
848
            case Lexer::T_REAL:
849
                $this->match(Lexer::T_REAL);
850
                $dataType = new AST\DataType\RealDataType(
851
                    $this->dataTypeDecimals(),
852
                    $this->numericDataTypeOptions()
853
                );
854
                break;
855
            case Lexer::T_DOUBLE:
856
                $this->match(Lexer::T_DOUBLE);
857
                if ($this->lexer->isNextToken(Lexer::T_PRECISION)) {
858
                    $this->match(Lexer::T_PRECISION);
859
                }
860
                $dataType = new AST\DataType\DoubleDataType(
861
                    $this->dataTypeDecimals(),
862
                    $this->numericDataTypeOptions()
863
                );
864
                break;
865
            case Lexer::T_FLOAT:
866
                $this->match(Lexer::T_FLOAT);
867
                $dataType = new AST\DataType\FloatDataType(
868
                    $this->dataTypeDecimals(),
869
                    $this->numericDataTypeOptions()
870
                );
871
872
                break;
873
            case Lexer::T_DECIMAL:
874
                $this->match(Lexer::T_DECIMAL);
875
                $dataType = new AST\DataType\DecimalDataType(
876
                    $this->dataTypeDecimals(),
877
                    $this->numericDataTypeOptions()
878
                );
879
                break;
880
            case Lexer::T_NUMERIC:
881
                $this->match(Lexer::T_NUMERIC);
882
                $dataType = new AST\DataType\NumericDataType(
883
                    $this->dataTypeDecimals(),
884
                    $this->numericDataTypeOptions()
885
                );
886
                break;
887
            case Lexer::T_DATE:
888
                $this->match(Lexer::T_DATE);
889
                $dataType = new AST\DataType\DateDataType();
890
                break;
891
            case Lexer::T_TIME:
892
                $this->match(Lexer::T_TIME);
893
                $dataType = new AST\DataType\TimeDataType($this->fractionalSecondsPart());
894
                break;
895
            case Lexer::T_TIMESTAMP:
896
                $this->match(Lexer::T_TIMESTAMP);
897
                $dataType = new AST\DataType\TimestampDataType($this->fractionalSecondsPart());
898
                break;
899
            case Lexer::T_DATETIME:
900
                $this->match(Lexer::T_DATETIME);
901
                $dataType = new AST\DataType\DateTimeDataType($this->fractionalSecondsPart());
902
                break;
903
            case Lexer::T_YEAR:
904
                $this->match(Lexer::T_YEAR);
905
                $dataType = new AST\DataType\YearDataType();
906
                break;
907
            case Lexer::T_CHAR:
908
                $this->match(Lexer::T_CHAR);
909
                $dataType = new AST\DataType\CharDataType(
910
                    $this->dataTypeLength(),
911
                    $this->characterDataTypeOptions()
912
                );
913
                break;
914
            case Lexer::T_VARCHAR:
915
                $this->match(Lexer::T_VARCHAR);
916
                $dataType = new AST\DataType\VarCharDataType(
917
                    $this->dataTypeLength(true),
918
                    $this->characterDataTypeOptions()
919
                );
920
                break;
921
            case Lexer::T_BINARY:
922
                $this->match(Lexer::T_BINARY);
923
                $dataType = new AST\DataType\BinaryDataType($this->dataTypeLength());
924
                break;
925
            case Lexer::T_VARBINARY:
926
                $this->match(Lexer::T_VARBINARY);
927
                $dataType = new AST\DataType\VarBinaryDataType($this->dataTypeLength(true));
928
                break;
929
            case Lexer::T_TINYBLOB:
930
                $this->match(Lexer::T_TINYBLOB);
931
                $dataType = new AST\DataType\TinyBlobDataType();
932
                break;
933
            case Lexer::T_BLOB:
934
                $this->match(Lexer::T_BLOB);
935
                $dataType = new AST\DataType\BlobDataType();
936
                break;
937
            case Lexer::T_MEDIUMBLOB:
938
                $this->match(Lexer::T_MEDIUMBLOB);
939
                $dataType = new AST\DataType\MediumBlobDataType();
940
                break;
941
            case Lexer::T_LONGBLOB:
942
                $this->match(Lexer::T_LONGBLOB);
943
                $dataType = new AST\DataType\LongBlobDataType();
944
                break;
945
            case Lexer::T_TINYTEXT:
946
                $this->match(Lexer::T_TINYTEXT);
947
                $dataType = new AST\DataType\TinyTextDataType($this->characterDataTypeOptions());
948
                break;
949
            case Lexer::T_TEXT:
950
                $this->match(Lexer::T_TEXT);
951
                $dataType = new AST\DataType\TextDataType($this->characterDataTypeOptions());
952
                break;
953
            case Lexer::T_MEDIUMTEXT:
954
                $this->match(Lexer::T_MEDIUMTEXT);
955
                $dataType = new AST\DataType\MediumTextDataType($this->characterDataTypeOptions());
956
                break;
957
            case Lexer::T_LONGTEXT:
958
                $this->match(Lexer::T_LONGTEXT);
959
                $dataType = new AST\DataType\LongTextDataType($this->characterDataTypeOptions());
960
                break;
961
            case Lexer::T_ENUM:
962
                $this->match(Lexer::T_ENUM);
963
                $dataType = new AST\DataType\EnumDataType($this->valueList(), $this->enumerationDataTypeOptions());
964
                break;
965
            case Lexer::T_SET:
966
                $this->match(Lexer::T_SET);
967
                $dataType = new AST\DataType\SetDataType($this->valueList(), $this->enumerationDataTypeOptions());
968
                break;
969
            case Lexer::T_JSON:
970
                $this->match(Lexer::T_JSON);
971
                $dataType = new AST\DataType\JsonDataType();
972
                break;
973
            default:
974
                $this->syntaxError(
975
                    'BIT, TINYINT, SMALLINT, MEDIUMINT, INT, INTEGER, BIGINT, REAL, DOUBLE, FLOAT, DECIMAL, NUMERIC, ' .
976
                    'DATE, TIME, TIMESTAMP, DATETIME, YEAR, CHAR, VARCHAR, BINARY, VARBINARY, TINYBLOB, BLOB, ' .
977
                    'MEDIUMBLOB, LONGBLOB, TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT, ENUM, SET, or JSON'
978
                );
979
        }
980
981
        return $dataType;
982
    }
983
984
    /**
985
     * DefaultValue::= DEFAULT default_value
986
     *
987
     * @return mixed
988
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
989
     */
990
    protected function columnDefaultValue()
991
    {
992
        $this->match(Lexer::T_DEFAULT);
993
        $value = null;
994
995
        switch ($this->lexer->lookahead['type']) {
996
            case Lexer::T_INTEGER:
997
                $value = (int)$this->lexer->lookahead['value'];
998
                break;
999
            case Lexer::T_FLOAT:
1000
                $value = (float)$this->lexer->lookahead['value'];
1001
                break;
1002
            case Lexer::T_STRING:
1003
                $value = (string)$this->lexer->lookahead['value'];
1004
                break;
1005
            case Lexer::T_CURRENT_TIMESTAMP:
1006
                $value = 'CURRENT_TIMESTAMP';
1007
                break;
1008
            case Lexer::T_NULL:
1009
                $value = null;
1010
                break;
1011
            default:
1012
                $this->syntaxError('String, Integer, Float, NULL or CURRENT_TIMESTAMP');
1013
        }
1014
1015
        $this->lexer->moveNext();
1016
1017
        return $value;
1018
    }
1019
1020
    /**
1021
     * Determine length parameter of a column field definition, i.E. INT(11) or VARCHAR(255)
1022
     *
1023
     * @param bool $required
1024
     * @return int
1025
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1026
     */
1027
    protected function dataTypeLength(bool $required = false): int
1028
    {
1029
        $length = 0;
1030
        if (!$this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
1031
            if ($required) {
1032
                $this->semanticalError('The current data type requires a field length definition.');
1033
            }
1034
            return $length;
1035
        }
1036
1037
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1038
        $length = (int)$this->lexer->lookahead['value'];
1039
        $this->match(Lexer::T_INTEGER);
1040
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1041
1042
        return $length;
1043
    }
1044
1045
    /**
1046
     * Determine length and optional decimal parameter of a column field definition, i.E. DECIMAL(10,6)
1047
     *
1048
     * @return array
1049
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1050
     */
1051
    private function dataTypeDecimals(): array
1052
    {
1053
        $options = [];
1054
        if (!$this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
1055
            return $options;
1056
        }
1057
1058
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1059
        $options['length'] = (int)$this->lexer->lookahead['value'];
1060
        $this->match(Lexer::T_INTEGER);
1061
1062
        if ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1063
            $this->match(Lexer::T_COMMA);
1064
            $options['decimals'] = (int)$this->lexer->lookahead['value'];
1065
            $this->match(Lexer::T_INTEGER);
1066
        }
1067
1068
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1069
1070
        return $options;
1071
    }
1072
1073
    /**
1074
     * Parse common options for numeric datatypes
1075
     *
1076
     * @return array
1077
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1078
     */
1079
    protected function numericDataTypeOptions(): array
1080
    {
1081
        $options = ['unsigned' => false, 'zerofill' => false];
1082
1083
        if (!$this->lexer->isNextTokenAny([Lexer::T_UNSIGNED, Lexer::T_ZEROFILL])) {
1084
            return $options;
1085
        }
1086
1087
        while ($this->lexer->isNextTokenAny([Lexer::T_UNSIGNED, Lexer::T_ZEROFILL])) {
1088
            switch ($this->lexer->lookahead['type']) {
1089
                case Lexer::T_UNSIGNED:
1090
                    $this->match(Lexer::T_UNSIGNED);
1091
                    $options['unsigned'] = true;
1092
                    break;
1093
                case Lexer::T_ZEROFILL:
1094
                    $this->match(Lexer::T_ZEROFILL);
1095
                    $options['zerofill'] = true;
1096
                    break;
1097
                default:
1098
                    $this->syntaxError('USIGNED or ZEROFILL');
1099
            }
1100
        }
1101
1102
        return $options;
1103
    }
1104
1105
    /**
1106
     * Determine the fractional seconds part support for TIME, DATETIME and TIMESTAMP columns
1107
     *
1108
     * @return int
1109
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1110
     */
1111
    protected function fractionalSecondsPart(): int
1112
    {
1113
        $fractionalSecondsPart = $this->dataTypeLength();
1114
        if ($fractionalSecondsPart < 0) {
1115
            $this->semanticalError('the fractional seconds part for TIME, DATETIME or TIMESTAMP columns must >= 0');
1116
        }
1117
        if ($fractionalSecondsPart > 6) {
1118
            $this->semanticalError('the fractional seconds part for TIME, DATETIME or TIMESTAMP columns must <= 6');
1119
        }
1120
1121
        return $fractionalSecondsPart;
1122
    }
1123
1124
    /**
1125
     * Parse common options for numeric datatypes
1126
     *
1127
     * @return array
1128
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1129
     */
1130
    protected function characterDataTypeOptions(): array
1131
    {
1132
        $options = ['binary' => false, 'charset' => null, 'collation' => null];
1133
1134
        if (!$this->lexer->isNextTokenAny([Lexer::T_CHARACTER, Lexer::T_COLLATE, Lexer::T_BINARY])) {
1135
            return $options;
1136
        }
1137
1138
        while ($this->lexer->isNextTokenAny([Lexer::T_CHARACTER, Lexer::T_COLLATE, Lexer::T_BINARY])) {
1139
            switch ($this->lexer->lookahead['type']) {
1140
                case Lexer::T_BINARY:
1141
                    $this->match(Lexer::T_BINARY);
1142
                    $options['binary'] = true;
1143
                    break;
1144
                case Lexer::T_CHARACTER:
1145
                    $this->match(Lexer::T_CHARACTER);
1146
                    $this->match(Lexer::T_SET);
1147
                    $this->match(Lexer::T_STRING);
1148
                    $options['charset'] = $this->lexer->token['value'];
1149
                    break;
1150
                case Lexer::T_COLLATE:
1151
                    $this->match(Lexer::T_COLLATE);
1152
                    $this->match(Lexer::T_STRING);
1153
                    $options['collation'] = $this->lexer->token['value'];
1154
                    break;
1155
                default:
1156
                    $this->syntaxError('BINARY, CHARACTER SET or COLLATE');
1157
            }
1158
        }
1159
1160
        return $options;
1161
    }
1162
1163
    /**
1164
     * Parse shared options for enumeration datatypes (ENUM and SET)
1165
     *
1166
     * @return array
1167
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1168
     */
1169
    protected function enumerationDataTypeOptions(): array
1170
    {
1171
        $options = ['charset' => null, 'collation' => null];
1172
1173
        if (!$this->lexer->isNextTokenAny([Lexer::T_CHARACTER, Lexer::T_COLLATE])) {
1174
            return $options;
1175
        }
1176
1177
        while ($this->lexer->isNextTokenAny([Lexer::T_CHARACTER, Lexer::T_COLLATE])) {
1178
            switch ($this->lexer->lookahead['type']) {
1179
                case Lexer::T_CHARACTER:
1180
                    $this->match(Lexer::T_CHARACTER);
1181
                    $this->match(Lexer::T_SET);
1182
                    $this->match(Lexer::T_STRING);
1183
                    $options['charset'] = $this->lexer->token['value'];
1184
                    break;
1185
                case Lexer::T_COLLATE:
1186
                    $this->match(Lexer::T_COLLATE);
1187
                    $this->match(Lexer::T_STRING);
1188
                    $options['collation'] = $this->lexer->token['value'];
1189
                    break;
1190
                default:
1191
                    $this->syntaxError('CHARACTER SET or COLLATE');
1192
            }
1193
        }
1194
1195
        return $options;
1196
    }
1197
1198
    /**
1199
     * Return all defined values for an enumeration datatype (ENUM, SET)
1200
     *
1201
     * @return array
1202
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1203
     */
1204
    protected function valueList(): array
1205
    {
1206
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1207
1208
        $values = [];
1209
        $values[] = $this->valueListItem();
1210
1211
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1212
            $this->match(Lexer::T_COMMA);
1213
            $values[] = $this->valueListItem();
1214
        }
1215
1216
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1217
1218
        return $values;
1219
    }
1220
1221
    /**
1222
     * Return a value list item for an enumeration set
1223
     *
1224
     * @return string
1225
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1226
     */
1227
    protected function valueListItem(): string
1228
    {
1229
        $this->match(Lexer::T_STRING);
1230
1231
        return (string)$this->lexer->token['value'];
1232
    }
1233
1234
    /**
1235
     * ReferenceDefinition ::= REFERENCES tbl_name (index_col_name,...)
1236
     *  [MATCH FULL | MATCH PARTIAL | MATCH SIMPLE]
1237
     *  [ON DELETE reference_option]
1238
     *  [ON UPDATE reference_option]
1239
     *
1240
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\ReferenceDefinition
1241
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1242
     */
1243
    protected function referenceDefinition(): AST\ReferenceDefinition
1244
    {
1245
        $this->match(Lexer::T_REFERENCES);
1246
        $tableName = $this->schemaObjectName();
1247
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1248
1249
        $referenceColumns = [];
1250
        $referenceColumns[] = $this->indexColumnName();
1251
1252
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1253
            $this->match(Lexer::T_COMMA);
1254
            $referenceColumns[] = $this->indexColumnName();
1255
        }
1256
1257
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1258
1259
        $referenceDefinition = new AST\ReferenceDefinition($tableName, $referenceColumns);
1260
1261
        while (!$this->lexer->isNextTokenAny([Lexer::T_COMMA, Lexer::T_CLOSE_PARENTHESIS])) {
1262
            switch ($this->lexer->lookahead['type']) {
1263
                case Lexer::T_MATCH:
1264
                    $this->match(Lexer::T_MATCH);
1265
                    $referenceDefinition->match = $this->lexer->lookahead['value'];
1266
                    $this->lexer->moveNext();
1267
                    break;
1268
                case Lexer::T_ON:
1269
                    $this->match(Lexer::T_ON);
1270
                    if ($this->lexer->isNextToken(Lexer::T_DELETE)) {
1271
                        $this->match(Lexer::T_DELETE);
1272
                        $referenceDefinition->onDelete = $this->referenceOption();
1273
                    } else {
1274
                        $this->match(Lexer::T_UPDATE);
1275
                        $referenceDefinition->onUpdate = $this->referenceOption();
1276
                    }
1277
                    break;
1278
                default:
1279
                    $this->syntaxError('MATCH, ON DELETE or ON UPDATE');
1280
            }
1281
        }
1282
1283
        return $referenceDefinition;
1284
    }
1285
1286
    /**
1287
     * IndexColumnName ::= col_name [(length)] [ASC | DESC]
1288
     *
1289
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\IndexColumnName
1290
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1291
     */
1292
    protected function indexColumnName(): AST\IndexColumnName
1293
    {
1294
        $columnName = $this->schemaObjectName();
1295
        $length = $this->dataTypeLength();
1296
        $direction = null;
1297
1298
        if ($this->lexer->isNextToken(Lexer::T_ASC)) {
1299
            $this->match(Lexer::T_ASC);
1300
            $direction = 'ASC';
1301
        } elseif ($this->lexer->isNextToken(Lexer::T_DESC)) {
1302
            $this->match(Lexer::T_DESC);
1303
            $direction = 'DESC';
1304
        }
1305
1306
        return new AST\IndexColumnName($columnName, $length, $direction);
1307
    }
1308
1309
    /**
1310
     * ReferenceOption ::= RESTRICT | CASCADE | SET NULL | NO ACTION
1311
     *
1312
     * @return string
1313
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1314
     */
1315
    protected function referenceOption(): string
1316
    {
1317
        $action = null;
1318
1319
        switch ($this->lexer->lookahead['type']) {
1320
            case Lexer::T_RESTRICT:
1321
                $this->match(Lexer::T_RESTRICT);
1322
                $action = 'RESTRICT';
1323
                break;
1324
            case Lexer::T_CASCADE:
1325
                $this->match(Lexer::T_CASCADE);
1326
                $action = 'CASCADE';
1327
                break;
1328
            case Lexer::T_SET:
1329
                $this->match(Lexer::T_SET);
1330
                $this->match(Lexer::T_NULL);
1331
                $action = 'SET NULL';
1332
                break;
1333
            case Lexer::T_NO:
1334
                $this->match(Lexer::T_NO);
1335
                $this->match(Lexer::T_ACTION);
1336
                $action = 'NO ACTION';
1337
                break;
1338
            default:
1339
                $this->syntaxError('RESTRICT, CASCADE, SET NULL or NO ACTION');
1340
        }
1341
1342
        return $action;
1343
    }
1344
1345
    /**
1346
     * Parse MySQL table options
1347
     *
1348
     *  ENGINE [=] engine_name
1349
     *  | AUTO_INCREMENT [=] value
1350
     *  | AVG_ROW_LENGTH [=] value
1351
     *  | [DEFAULT] CHARACTER SET [=] charset_name
1352
     *  | CHECKSUM [=] {0 | 1}
1353
     *  | [DEFAULT] COLLATE [=] collation_name
1354
     *  | COMMENT [=] 'string'
1355
     *  | COMPRESSION [=] {'ZLIB'|'LZ4'|'NONE'}
1356
     *  | CONNECTION [=] 'connect_string'
1357
     *  | DATA DIRECTORY [=] 'absolute path to directory'
1358
     *  | DELAY_KEY_WRITE [=] {0 | 1}
1359
     *  | ENCRYPTION [=] {'Y' | 'N'}
1360
     *  | INDEX DIRECTORY [=] 'absolute path to directory'
1361
     *  | INSERT_METHOD [=] { NO | FIRST | LAST }
1362
     *  | KEY_BLOCK_SIZE [=] value
1363
     *  | MAX_ROWS [=] value
1364
     *  | MIN_ROWS [=] value
1365
     *  | PACK_KEYS [=] {0 | 1 | DEFAULT}
1366
     *  | PASSWORD [=] 'string'
1367
     *  | ROW_FORMAT [=] {DEFAULT|DYNAMIC|FIXED|COMPRESSED|REDUNDANT|COMPACT}
1368
     *  | STATS_AUTO_RECALC [=] {DEFAULT|0|1}
1369
     *  | STATS_PERSISTENT [=] {DEFAULT|0|1}
1370
     *  | STATS_SAMPLE_PAGES [=] value
1371
     *  | TABLESPACE tablespace_name
1372
     *  | UNION [=] (tbl_name[,tbl_name]...)
1373
     *
1374
     * @return array
1375
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1376
     */
1377
    protected function tableOptions(): array
1378
    {
1379
        $options = [];
1380
1381
        while ($this->lexer->lookahead && !$this->lexer->isNextToken(Lexer::T_SEMICOLON)) {
1382
            switch ($this->lexer->lookahead['type']) {
1383
                case Lexer::T_DEFAULT:
1384
                    // DEFAULT prefix is optional for COLLATE/CHARACTER SET, do nothing
1385
                    $this->match(Lexer::T_DEFAULT);
1386
                    break;
1387
                case Lexer::T_ENGINE:
1388
                    $this->match(Lexer::T_ENGINE);
1389
                    $options['engine'] = (string)$this->tableOptionValue();
1390
                    break;
1391
                case Lexer::T_AUTO_INCREMENT:
1392
                    $this->match(Lexer::T_AUTO_INCREMENT);
1393
                    $options['auto_increment'] = (int)$this->tableOptionValue();
1394
                    break;
1395
                case Lexer::T_AVG_ROW_LENGTH:
1396
                    $this->match(Lexer::T_AVG_ROW_LENGTH);
1397
                    $options['average_row_length'] = (int)$this->tableOptionValue();
1398
                    break;
1399
                case Lexer::T_CHARACTER:
1400
                    $this->match(Lexer::T_CHARACTER);
1401
                    $this->match(Lexer::T_SET);
1402
                    $options['character_set'] = (string)$this->tableOptionValue();
1403
                    break;
1404
                case Lexer::T_CHECKSUM:
1405
                    $this->match(Lexer::T_CHECKSUM);
1406
                    $options['checksum'] = (int)$this->tableOptionValue();
1407
                    break;
1408
                case Lexer::T_COLLATE:
1409
                    $this->match(Lexer::T_COLLATE);
1410
                    $options['collation'] = (string)$this->tableOptionValue();
1411
                    break;
1412
                case Lexer::T_COMMENT:
1413
                    $this->match(Lexer::T_COMMENT);
1414
                    $options['comment'] = (string)$this->tableOptionValue();
1415
                    break;
1416
                case Lexer::T_COMPRESSION:
1417
                    $this->match(Lexer::T_COMPRESSION);
1418
                    $options['compression'] = strtoupper((string)$this->tableOptionValue());
1419
                    if (!in_array($options['compression'], ['ZLIB', 'LZ4', 'NONE'], true)) {
1420
                        $this->syntaxError('ZLIB, LZ4 or NONE', $this->lexer->token);
1421
                    }
1422
                    break;
1423
                case Lexer::T_CONNECTION:
1424
                    $this->match(Lexer::T_CONNECTION);
1425
                    $options['connection'] = (string)$this->tableOptionValue();
1426
                    break;
1427
                case Lexer::T_DATA:
1428
                    $this->match(Lexer::T_DATA);
1429
                    $this->match(Lexer::T_DIRECTORY);
1430
                    $options['data_directory'] = (string)$this->tableOptionValue();
1431
                    break;
1432
                case Lexer::T_DELAY_KEY_WRITE:
1433
                    $this->match(Lexer::T_DELAY_KEY_WRITE);
1434
                    $options['delay_key_write'] = (int)$this->tableOptionValue();
1435
                    break;
1436
                case Lexer::T_ENCRYPTION:
1437
                    $this->match(Lexer::T_ENCRYPTION);
1438
                    $options['encryption'] = strtoupper((string)$this->tableOptionValue());
1439
                    if (!in_array($options['encryption'], ['Y', 'N'], true)) {
1440
                        $this->syntaxError('Y or N', $this->lexer->token);
1441
                    }
1442
                    break;
1443
                case Lexer::T_INDEX:
1444
                    $this->match(Lexer::T_INDEX);
1445
                    $this->match(Lexer::T_DIRECTORY);
1446
                    $options['index_directory'] = (string)$this->tableOptionValue();
1447
                    break;
1448
                case Lexer::T_INSERT_METHOD:
1449
                    $this->match(Lexer::T_INSERT_METHOD);
1450
                    $options['insert_method'] = strtoupper((string)$this->tableOptionValue());
1451
                    if (!in_array($options['insert_method'], ['NO', 'FIRST', 'LAST'], true)) {
1452
                        $this->syntaxError('NO, FIRST or LAST', $this->lexer->token);
1453
                    }
1454
                    break;
1455
                case Lexer::T_KEY_BLOCK_SIZE:
1456
                    $this->match(Lexer::T_KEY_BLOCK_SIZE);
1457
                    $options['key_block_size'] = (int)$this->tableOptionValue();
1458
                    break;
1459
                case Lexer::T_MAX_ROWS:
1460
                    $this->match(Lexer::T_MAX_ROWS);
1461
                    $options['max_rows'] = (int)$this->tableOptionValue();
1462
                    break;
1463
                case Lexer::T_MIN_ROWS:
1464
                    $this->match(Lexer::T_MIN_ROWS);
1465
                    $options['min_rows'] = (int)$this->tableOptionValue();
1466
                    break;
1467
                case Lexer::T_PACK_KEYS:
1468
                    $this->match(Lexer::T_PACK_KEYS);
1469
                    $options['pack_keys'] = strtoupper((string)$this->tableOptionValue());
1470
                    if (!in_array($options['pack_keys'], ['0', '1', 'DEFAULT'], true)) {
1471
                        $this->syntaxError('0, 1 or DEFAULT', $this->lexer->token);
1472
                    }
1473
                    break;
1474
                case Lexer::T_PASSWORD:
1475
                    $this->match(Lexer::T_PASSWORD);
1476
                    $options['password'] = (string)$this->tableOptionValue();
1477
                    break;
1478
                case Lexer::T_ROW_FORMAT:
1479
                    $this->match(Lexer::T_ROW_FORMAT);
1480
                    $options['row_format'] = (string)$this->tableOptionValue();
1481
                    $validRowFormats = ['DEFAULT', 'DYNAMIC', 'FIXED', 'COMPRESSED', 'REDUNDANT', 'COMPACT'];
1482
                    if (!in_array($options['row_format'], $validRowFormats, true)) {
1483
                        $this->syntaxError(
1484
                            'DEFAULT, DYNAMIC, FIXED, COMPRESSED, REDUNDANT, COMPACT',
1485
                            $this->lexer->token
1486
                        );
1487
                    }
1488
                    break;
1489
                case Lexer::T_STATS_AUTO_RECALC:
1490
                    $this->match(Lexer::T_STATS_AUTO_RECALC);
1491
                    $options['stats_auto_recalc'] = strtoupper((string)$this->tableOptionValue());
1492
                    if (!in_array($options['stats_auto_recalc'], ['0', '1', 'DEFAULT'], true)) {
1493
                        $this->syntaxError('0, 1 or DEFAULT', $this->lexer->token);
1494
                    }
1495
                    break;
1496
                case Lexer::T_STATS_PERSISTENT:
1497
                    $this->match(Lexer::T_STATS_PERSISTENT);
1498
                    $options['stats_persistent'] = strtoupper((string)$this->tableOptionValue());
1499
                    if (!in_array($options['stats_persistent'], ['0', '1', 'DEFAULT'], true)) {
1500
                        $this->syntaxError('0, 1 or DEFAULT', $this->lexer->token);
1501
                    }
1502
                    break;
1503
                case Lexer::T_STATS_SAMPLE_PAGES:
1504
                    $this->match(Lexer::T_STATS_SAMPLE_PAGES);
1505
                    $options['stats_sample_pages'] = strtoupper((string)$this->tableOptionValue());
1506
                    if (!in_array($options['stats_sample_pages'], ['0', '1', 'DEFAULT'], true)) {
1507
                        $this->syntaxError('0, 1 or DEFAULT', $this->lexer->token);
1508
                    }
1509
                    break;
1510
                case Lexer::T_TABLESPACE:
1511
                    $this->match(Lexer::T_TABLESPACE);
1512
                    $options['tablespace'] = (string)$this->tableOptionValue();
1513
                    break;
1514
                default:
1515
                    $this->syntaxError(
1516
                        'DEFAULT, ENGINE, AUTO_INCREMENT, AVG_ROW_LENGTH, CHARACTER SET, ' .
1517
                        'CHECKSUM, COLLATE, COMMENT, COMPRESSION, CONNECTION, DATA DIRECTORY, ' .
1518
                        'DELAY_KEY_WRITE, ENCRYPTION, INDEX DIRECTORY, INSERT_METHOD, KEY_BLOCK_SIZE, ' .
1519
                        'MAX_ROWS, MIN_ROWS, PACK_KEYS, PASSWORD, ROW_FORMAT, STATS_AUTO_RECALC, ' .
1520
                        'STATS_PERSISTENT, STATS_SAMPLE_PAGES or TABLESPACE'
1521
                    );
1522
            }
1523
        }
1524
1525
        return $options;
1526
    }
1527
1528
    /**
1529
     * Return the value of an option, skipping the optional equal sign.
1530
     *
1531
     * @return mixed
1532
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1533
     */
1534
    protected function tableOptionValue()
1535
    {
1536
        // Skip the optional equals sign
1537
        if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
1538
            $this->match(Lexer::T_EQUALS);
1539
        }
1540
        $this->lexer->moveNext();
1541
1542
        return $this->lexer->token['value'];
1543
    }
1544
1545
    /**
1546
     * Certain objects within MySQL, including database, table, index, column, alias, view, stored procedure,
1547
     * partition, tablespace, and other object names are known as identifiers.
1548
     *
1549
     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\Identifier
1550
     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
1551
     */
1552
    protected function schemaObjectName()
1553
    {
1554
        $schemaObjectName = $this->lexer->lookahead['value'];
1555
        $this->lexer->moveNext();
1556
1557
        return new AST\Identifier((string)$schemaObjectName);
1558
    }
1559
}
1560