[BUGFIX] Fix several typos in php comments
[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 *
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 a 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;
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 }