TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
Classes/Database/Schema/Parser/Parser.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types=1);
3 
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;
22 
27 class Parser
28 {
34  protected $lexer;
35 
41  protected $statement;
42 
48  public function __construct(string $statement)
49  {
50  $this->statement = $statement;
51  $this->lexer = new Lexer($statement);
52  }
53 
59  public function getLexer(): Lexer
60  {
61  return $this->lexer;
62  }
63 
70  public function getAST(): AST\AbstractCreateStatement
71  {
72  // Parse & build AST
73  return $this->queryLanguage();
74  }
75 
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 
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 
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 
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 
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 
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']) {
241  ++$numUnmatched;
242  break;
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 
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 
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 
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 
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 
373  protected function createDefinition(): AST\CreateDefinition
374  {
375  $createDefinitions = [];
376 
377  // Process opening 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
397 
398  return new AST\CreateDefinition($createDefinitions);
399  }
400 
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 
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 
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 
528 
529  $indexDefinition->options = $this->indexOptions();
530 
531  return $indexDefinition;
532  }
533 
541  {
542  $this->match(Lexer::T_FOREIGN);
543  $this->match(Lexer::T_KEY);
544 
545  $indexName = $this->indexName();
546 
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 
558 
559  $foreignKeyDefinition = new AST\CreateForeignKeyDefinitionItem(
560  $indexName,
561  $indexColumns,
562  $this->referenceDefinition()
563  );
564 
565  return $foreignKeyDefinition;
566  }
567 
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 
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 
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']) {
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 
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;
697  $columnDefinitionItem->autoIncrement = true;
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;
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 
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);
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);
961  break;
962  case Lexer::T_LONGTEXT:
963  $this->match(Lexer::T_LONGTEXT);
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 
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;
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 
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 
1043  $length = (int)$this->lexer->lookahead['value'];
1044  $this->match(Lexer::T_INTEGER);
1046 
1047  return $length;
1048  }
1049 
1056  private function dataTypeDecimals(): array
1057  {
1058  $options = [];
1059  if (!$this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
1060  return $options;
1061  }
1062 
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 
1074 
1075  return $options;
1076  }
1077 
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 
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 
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 
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 
1209  protected function valueList(): array
1210  {
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 
1222 
1223  return $values;
1224  }
1225 
1232  protected function valueListItem(): string
1233  {
1234  $this->match(Lexer::T_STRING);
1235 
1236  return (string)$this->lexer->token['value'];
1237  }
1238 
1248  protected function referenceDefinition(): AST\ReferenceDefinition
1249  {
1250  $this->match(Lexer::T_REFERENCES);
1251  $tableName = $this->schemaObjectName();
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 
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 
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 
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 
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;
1398  $options['auto_increment'] = (int)$this->tableOptionValue();
1399  break;
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;
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;
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;
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;
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;
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;
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 
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 
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 }
static syntaxError(string $message,\Exception $previous=null)
static semanticalError(string $message,\Exception $previous=null)