TYPO3 CMS  TYPO3_8-7
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 
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 
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 
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 
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 
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 
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 
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']) {
236  ++$numUnmatched;
237  break;
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 
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 
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 
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 
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 
368  protected function createDefinition(): AST\CreateDefinition
369  {
370  $createDefinitions = [];
371 
372  // Process opening 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
392 
393  return new AST\CreateDefinition($createDefinitions);
394  }
395 
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 
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 
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 
523 
524  $indexDefinition->options = $this->indexOptions();
525 
526  return $indexDefinition;
527  }
528 
536  {
537  $this->match(Lexer::T_FOREIGN);
538  $this->match(Lexer::T_KEY);
539 
540  $indexName = $this->indexName();
541 
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 
553 
554  $foreignKeyDefinition = new AST\CreateForeignKeyDefinitionItem(
555  $indexName,
556  $indexColumns,
557  $this->referenceDefinition()
558  );
559 
560  return $foreignKeyDefinition;
561  }
562 
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 
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 
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']) {
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 
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;
692  $columnDefinitionItem->autoIncrement = true;
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;
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 
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);
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);
956  break;
957  case Lexer::T_LONGTEXT:
958  $this->match(Lexer::T_LONGTEXT);
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 
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;
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 
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 
1038  $length = (int)$this->lexer->lookahead['value'];
1039  $this->match(Lexer::T_INTEGER);
1041 
1042  return $length;
1043  }
1044 
1051  private function dataTypeDecimals(): array
1052  {
1053  $options = [];
1054  if (!$this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
1055  return $options;
1056  }
1057 
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 
1069 
1070  return $options;
1071  }
1072 
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 
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 
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 
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 
1204  protected function valueList(): array
1205  {
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 
1217 
1218  return $values;
1219  }
1220 
1227  protected function valueListItem(): string
1228  {
1229  $this->match(Lexer::T_STRING);
1230 
1231  return (string)$this->lexer->token['value'];
1232  }
1233 
1243  protected function referenceDefinition(): AST\ReferenceDefinition
1244  {
1245  $this->match(Lexer::T_REFERENCES);
1246  $tableName = $this->schemaObjectName();
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 
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 
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 
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 
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;
1393  $options['auto_increment'] = (int)$this->tableOptionValue();
1394  break;
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;
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;
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;
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;
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;
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;
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 
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 
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 }
semanticalError($message='', $token=null)
Definition: Parser.php:196
static syntaxError(string $message, \Exception $previous=null)
syntaxError($expected='', $token=null)
Definition: Parser.php:172
static semanticalError(string $message, \Exception $previous=null)
free($deep=false, $position=0)
Definition: Parser.php:126