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