[FEATURE] Doctrine: Implement SchemaMigrationService 93/49593/16
authorMorton Jonuschat <m.jonuschat@mojocode.de>
Wed, 24 Aug 2016 12:57:46 +0000 (14:57 +0200)
committerMorton Jonuschat <m.jonuschat@mojocode.de>
Wed, 31 Aug 2016 20:28:51 +0000 (22:28 +0200)
Implement a SQL schema migration service based on an actual
parser for CREATE TABLE statements that are mapped to Doctrine
Table objects. This enables the use of the Doctrine DBAL
SchemaManager for all schema modifications.

The new Schema migration service is fully aware of multiple
database connections and normalizes MySQL specific data types
to standard compliant types. This mostly affects the TINYINT
data type which gets converted to a SMALLINT.

Resolves: #77643
Resolves: #77369
Resolves: #76508
Resolves: #76641
Resolves: #75205
Resolves: #71645
Resolves: #44991
Releases: master
Change-Id: Ic56941c2ae9717836d89bce74261d11424da340b
Reviewed-on: https://review.typo3.org/49593
Tested-by: Bamboo TYPO3com <info@typo3.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
106 files changed:
typo3/sysext/core/Classes/Database/Connection.php
typo3/sysext/core/Classes/Database/ConnectionPool.php
typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaColumnDefinitionListener.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Exception/StatementException.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Exception/UnexpectedSignalReturnValueTypeException.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/AbstractCreateDefinitionItem.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/AbstractCreateStatement.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateColumnDefinitionItem.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateDefinition.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateForeignKeyDefinitionItem.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateIndexDefinitionItem.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateTableClause.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateTableStatement.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/AbstractDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/BigIntDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/BinaryDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/BitDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/BlobDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/CharDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/DateDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/DateTimeDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/DecimalDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/DoubleDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/EnumDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/FloatDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/IntegerDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/JsonDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/LongBlobDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/LongTextDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/MediumBlobDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/MediumIntDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/MediumTextDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/NumericDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/RealDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/SetDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/SmallIntDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TextDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TimeDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TimestampDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TinyBlobDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TinyIntDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TinyTextDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/VarBinaryDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/VarCharDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/YearDataType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/Identifier.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/IndexColumnName.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/AST/ReferenceDefinition.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/Lexer.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/Parser.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Parser/TableBuilder.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/SchemaMigrator.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/SqlReader.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Types/EnumType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Schema/Types/SetType.php [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-77643-ReimplementSqlSchemaMigrationServiceUsingDoctrineSchemaManager.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Database/Fixtures/addColumnsToTable.sql [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Database/Fixtures/addCreateChange.sql [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Database/Fixtures/changeExistingColumn.sql [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Database/Fixtures/defaultNullWithoutNotNull.sql [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Database/Fixtures/ifNotExists.sql [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Database/Fixtures/importStaticData.sql [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Database/Fixtures/newTable.sql [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Database/Fixtures/notNullWithoutDefaultValue.sql [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Database/Fixtures/unusedColumn.sql [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Database/Fixtures/unusedTable.sql [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Database/Schema/SchemaMigrationServiceTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/EventListener/SchemaColumnDefinitionListenerTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Fixtures/tablebuilder.sql [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/AbstractDataTypeBaseTestCase.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/ColumnDefinitionAttributesTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/ColumnDefinitionItemTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/CreateTableFragmentTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypeAttributes/CharacterTypeAttributesTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypeAttributes/EnumerationTypeAttributesTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypeAttributes/NumericTypeAttributesTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/BinaryDataTypeTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/BitDataTypeTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/BlobTypesTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/CharDataTypeTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/DateTimeTypesTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/EnumDataTypeTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/FixedPointTypesTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/FloatingPointTypesTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/IntegerTypesTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/JsonDataTypeTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/SetDataTypeTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/TextTypesTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/ForeignKeyDefinitionTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/IndexDefinitionTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/ReferenceDefinitionTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/TableBuilderTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Parser/TableOptionsTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/SqlReaderTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Types/EnumTypeTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Schema/Types/SetTypeTest.php [new file with mode: 0644]
typo3/sysext/extensionmanager/Classes/Utility/InstallUtility.php
typo3/sysext/install/Classes/Controller/Action/Step/DatabaseData.php
typo3/sysext/install/Classes/Controller/Action/Tool/ImportantActions.php
typo3/sysext/install/Classes/Controller/Action/Tool/UpgradeWizard.php
typo3/sysext/install/Classes/Service/SqlExpectedSchemaService.php
typo3/sysext/install/Classes/Service/SqlSchemaMigrationService.php
typo3/sysext/install/Classes/Updates/AbstractDatabaseSchemaUpdate.php
typo3/sysext/install/Classes/Updates/ExtensionManagerTables.php
typo3/sysext/install/Classes/Updates/FinalDatabaseSchemaUpdate.php
typo3/sysext/install/Classes/Updates/InitialDatabaseSchemaUpdate.php

index ee67933..714554a 100644 (file)
@@ -307,7 +307,7 @@ class Connection extends \Doctrine\DBAL\Connection
             $query->andWhere($query->expr()->eq($identifier, $query->createNamedParameter($value)));
         }
 
-        return $query->execute()->fetchColumn(0);
+        return (int)$query->execute()->fetchColumn(0);
     }
 
     /**
index 85a9be4..b0040b2 100644 (file)
@@ -17,7 +17,13 @@ namespace TYPO3\CMS\Core\Database;
 
 use Doctrine\DBAL\Configuration;
 use Doctrine\DBAL\DriverManager;
+use Doctrine\DBAL\Events;
+use Doctrine\DBAL\Types\Type;
 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
+use TYPO3\CMS\Core\Database\Schema\SchemaColumnDefinitionListener;
+use TYPO3\CMS\Core\Database\Schema\Types\EnumType;
+use TYPO3\CMS\Core\Database\Schema\Types\SetType;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * Manager that handles opening/retrieving database connections.
@@ -42,6 +48,14 @@ class ConnectionPool
     protected static $connections = [];
 
     /**
+     * @var array
+     */
+    protected $customDoctrineTypes = [
+        EnumType::TYPE => EnumType::class,
+        SetType::TYPE => SetType::class,
+    ];
+
+    /**
      * Creates a connection object based on the specified table name.
      *
      * This is the official entry point to get a database connection to ensure
@@ -130,11 +144,31 @@ class ConnectionPool
         if (empty($connectionParams['charset'])) {
             $connectionParams['charset'] = 'utf-8';
         }
+
         /** @var Connection $conn */
         $conn = DriverManager::getConnection($connectionParams);
         $conn->setFetchMode(\PDO::FETCH_ASSOC);
         $conn->prepareConnection($connectionParams['initCommands'] ?? '');
 
+        // Register custom data types
+        foreach ($this->customDoctrineTypes as $type => $className) {
+            if (!Type::hasType($type)) {
+                Type::addType($type, $className);
+            }
+        }
+
+        // Register all custom data types in the type mapping
+        foreach ($this->customDoctrineTypes as $type => $className) {
+            $conn->getDatabasePlatform()->registerDoctrineTypeMapping($type, $type);
+        }
+
+        // Handler for building custom data type column definitions
+        // in the SchemaManager
+        $conn->getDatabasePlatform()->getEventManager()->addEventListener(
+            Events::onSchemaColumnDefinition,
+            GeneralUtility::makeInstance(SchemaColumnDefinitionListener::class)
+        );
+
         return $conn;
     }
 
@@ -170,4 +204,18 @@ class ConnectionPool
     {
         return array_keys($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']);
     }
+
+    /**
+     * Returns the list of custom Doctrine data types implemented by TYPO3.
+     * This method is needed by the Schema parser to register the types as it
+     * does not require a database connection and thus the types don't get
+     * registered automatically.
+     *
+     * @internal
+     * @return array
+     */
+    public function getCustomDoctrineTypes(): array
+    {
+        return $this->customDoctrineTypes;
+    }
 }
diff --git a/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaColumnDefinitionListener.php b/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaColumnDefinitionListener.php
new file mode 100644 (file)
index 0000000..5c61ef2
--- /dev/null
@@ -0,0 +1,127 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Doctrine\DBAL\Event\SchemaColumnDefinitionEventArgs;
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+use Doctrine\DBAL\Schema\Column;
+use Doctrine\DBAL\Types\Type;
+
+/**
+ * Event listener to handle additional processing for custom
+ * doctrine types.
+ */
+class SchemaColumnDefinitionListener
+{
+    /**
+     * Listener for column definition events. This intercepts definitions
+     * for custom doctrine types and builds the appropriate Column Object.
+     *
+     * @param \Doctrine\DBAL\Event\SchemaColumnDefinitionEventArgs $event
+     * @throws \Doctrine\DBAL\DBALException
+     */
+    public function onSchemaColumnDefinition(SchemaColumnDefinitionEventArgs $event)
+    {
+        $tableColumn = $event->getTableColumn();
+        $dbType = $this->getDatabaseType($tableColumn['Type']);
+        if ($dbType !== 'enum' && $dbType !== 'set') {
+            return;
+        }
+
+        $column = $this->getEnumerationTableColumnDefinition(
+            $tableColumn,
+            $event->getDatabasePlatform()
+        );
+
+        $event->setColumn($column);
+        $event->preventDefault();
+    }
+
+    /**
+     * Build a Doctrine column object for TYPE/TYPE columns.
+     *
+     * @param array $tableColumn
+     * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform
+     * @return \Doctrine\DBAL\Schema\Column
+     * @throws \Doctrine\DBAL\DBALException
+     * @todo: The $tableColumn source currently only support MySQL definition style.
+     */
+    protected function getEnumerationTableColumnDefinition(array $tableColumn, AbstractPlatform $platform): Column
+    {
+        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
+
+        $options = [
+            'length' => $tableColumn['length'] ?: null,
+            'unsigned' => false,
+            'fixed' => false,
+            'default' => $tableColumn['default'] ?: null,
+            'notnull' => (bool)($tableColumn['null'] !== 'YES'),
+            'scale' => null,
+            'precision' => null,
+            'autoincrement' => false,
+            'comment' => $tableColumn['comment'] ?: null,
+        ];
+
+        $dbType = $this->getDatabaseType($tableColumn['type']);
+        $doctrineType = $platform->getDoctrineTypeMapping($dbType);
+
+        $column = new Column($tableColumn['field'], Type::getType($doctrineType), $options);
+        $column->setPlatformOption('unquotedValues', $this->getUnquotedEnumerationValues($tableColumn['type']));
+
+        return $column;
+    }
+
+    /**
+     * Extract the field type from the definition string
+     *
+     * @param string $typeDefiniton
+     * @return string
+     */
+    protected function getDatabaseType(string $typeDefiniton): string
+    {
+        $dbType = strtolower($typeDefiniton);
+        $dbType = strtok($dbType, '(), ');
+
+        return $dbType;
+    }
+
+    /**
+     * @param string $typeDefiniton
+     * @return array
+     */
+    protected function getUnquotedEnumerationValues(string $typeDefiniton): array
+    {
+        $valuesDefinition = preg_replace('#^(enum|set)\((.*)\)\s*$#i', '$2', $typeDefiniton);
+        $quoteChar = $valuesDefinition[0];
+        $separator = $quoteChar . ',' . $quoteChar;
+
+        $valuesDefinition = preg_replace(
+            '#' . $quoteChar . ',\s*' . $quoteChar . '#',
+            $separator,
+            $valuesDefinition
+        );
+
+        $values = explode($quoteChar . ',' . $quoteChar, substr($valuesDefinition, 1, -1));
+
+        return array_map(
+            function (string $value) use ($quoteChar) {
+                return str_replace($quoteChar . $quoteChar, $quoteChar, $value);
+            },
+            $values
+        );
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Exception/StatementException.php b/typo3/sysext/core/Classes/Database/Schema/Exception/StatementException.php
new file mode 100644 (file)
index 0000000..ccb0adc
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Exception;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Class StatementException
+ */
+class StatementException extends \Exception
+{
+    /**
+     * @param string $sql
+     *
+     * @return StatementException
+     */
+    public static function sqlError(string $sql): StatementException
+    {
+        return new self($sql, 1471504820);
+    }
+
+    /**
+     * @param string $message
+     * @param \Exception|null $previous
+     *
+     * @return StatementException
+     */
+    public static function syntaxError(string $message, \Exception $previous = null): StatementException
+    {
+        return new self('[SQL Error] ' . $message, 1471504821, $previous);
+    }
+
+    /**
+     * @param string $message
+     * @param \Exception|null $previous
+     *
+     * @return StatementException
+     */
+    public static function semanticalError(string $message, \Exception $previous = null): StatementException
+    {
+        return new self('[Semantical Error] ' . $message, 1471504822, $previous);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Exception/UnexpectedSignalReturnValueTypeException.php b/typo3/sysext/core/Classes/Database/Schema/Exception/UnexpectedSignalReturnValueTypeException.php
new file mode 100644 (file)
index 0000000..6253239
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Exception;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * An exception thrown if the return value type of a signal is not the expected one.
+ */
+class UnexpectedSignalReturnValueTypeException extends \Exception
+{
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/AbstractCreateDefinitionItem.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/AbstractCreateDefinitionItem.php
new file mode 100644 (file)
index 0000000..149500e
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Base class for all definition items that can occur in the definition
+ * of a table, namely fields, indexes and foreign keys.
+ */
+abstract class AbstractCreateDefinitionItem
+{
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/AbstractCreateStatement.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/AbstractCreateStatement.php
new file mode 100644 (file)
index 0000000..a285985
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Base class for all create type statements like CREATE TABLE
+ * or CREATE VIEW.
+ */
+abstract class AbstractCreateStatement
+{
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateColumnDefinitionItem.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateColumnDefinitionItem.php
new file mode 100644 (file)
index 0000000..587e2e4
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\AbstractDataType;
+
+/**
+ * Syntax tree node for column definitions within a create statements.
+ * Holds basic attributes common to all types of columns.
+ */
+class CreateColumnDefinitionItem extends AbstractCreateDefinitionItem
+{
+    /**
+     * @var \TYPO3\CMS\Core\Database\Schema\Parser\AST\Identifier
+     */
+    public $columnName;
+
+    /**
+     * @var \TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\AbstractDataType
+     */
+    public $dataType;
+
+    /**
+     * Allow NULL values
+     *
+     * @var bool
+     */
+    public $allowNull = true;
+
+    /**
+     * Explicit default value
+     *
+     * @var bool
+     */
+    public $hasDefaultValue = false;
+
+    /**
+     * The explicit default value
+     *
+     * @var mixed
+     */
+    public $defaultValue;
+
+    /**
+     * Set auto increment flag
+     *
+     * @var bool
+     */
+    public $autoIncrement = false;
+
+    /**
+     * Create non-unique index for column
+     *
+     * @var bool
+     */
+    public $index = false;
+
+    /**
+     * Create unique constraint for column
+     *
+     * @var bool
+     */
+    public $unique = false;
+
+    /**
+     * Use column as primary key for table
+     *
+     * @var bool
+     */
+    public $primary = false;
+
+    /**
+     * Column comment
+     *
+     * @var string
+     */
+    public $comment;
+
+    /**
+     * The column format (DYNAMIC or FIXED)
+     *
+     * @var string
+     */
+    public $columnFormat;
+
+    /**
+     * The storage type for the column (ignored unless MySQL Cluster with NDB Engine)
+     *
+     * @var string
+     */
+    public $storage;
+
+    /**
+     * @var ReferenceDefinition
+     */
+    public $reference;
+
+    /**
+     * CreateColumnDefinitionItem constructor.
+     *
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\Identifier $columnName
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\AbstractDataType $dataType
+     */
+    public function __construct(Identifier $columnName, AbstractDataType $dataType)
+    {
+        $this->columnName = $columnName;
+        $this->dataType = $dataType;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateDefinition.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateDefinition.php
new file mode 100644 (file)
index 0000000..72d1c42
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Syntax node for the whole definition of a table/view. Collects
+ * the nodes for fields, indexes and foreign keys.
+ */
+class CreateDefinition
+{
+    /**
+     * @var array
+     */
+    public $items;
+
+    /**
+     * CreateDefinition constructor.
+     *
+     * @param array $items
+     */
+    public function __construct(array $items)
+    {
+        $this->items = $items;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateForeignKeyDefinitionItem.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateForeignKeyDefinitionItem.php
new file mode 100644 (file)
index 0000000..963540a
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Syntax node to structure a foreign key definition.
+ */
+class CreateForeignKeyDefinitionItem extends AbstractCreateDefinitionItem
+{
+    /**
+     * @var
+     */
+    public $indexName = '';
+
+    /**
+     * The index name
+     *
+     * @var string
+     */
+    public $name = '';
+
+    /**
+     * @var IndexColumnName[]
+     */
+    public $columnNames = [];
+
+    /**
+     * Reference definition
+     *
+     * @var ReferenceDefinition
+     */
+    public $reference;
+
+    /**
+     * CreateForeignKeyDefinitionItem constructor.
+     *
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\Identifier $indexName
+     * @param array $columnNames
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\ReferenceDefinition $reference
+     */
+    public function __construct(Identifier $indexName, array $columnNames, ReferenceDefinition $reference)
+    {
+        $this->indexName = $indexName;
+        $this->columnNames = $columnNames;
+        $this->reference = $reference;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateIndexDefinitionItem.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateIndexDefinitionItem.php
new file mode 100644 (file)
index 0000000..4adae6b
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Syntax node to structure an index definition.
+ */
+class CreateIndexDefinitionItem extends AbstractCreateDefinitionItem
+{
+    /**
+     * @var
+     */
+    public $indexName = '';
+
+    /**
+     * Create the primary key
+     *
+     * @var bool
+     */
+    public $isPrimary = false;
+
+    /**
+     * Create a unique index
+     *
+     * @var bool
+     */
+    public $isUnique = false;
+
+    /**
+     * Create a fulltext index
+     *
+     * @var bool
+     */
+    public $isFulltext = false;
+
+    /**
+     * Create a spatial (geo) index
+     *
+     * @var bool
+     */
+    public $isSpatial = false;
+
+    /**
+     * Use a special index type (MySQL: BTREE | HASH)
+     *
+     * @var string
+     */
+    public $indexType = '';
+
+    /**
+     * The index name
+     *
+     * @var string
+     */
+    public $name = '';
+
+    /**
+     * @var IndexColumnName[]
+     */
+    public $columnNames = [];
+
+    /**
+     * Index options KEY_BLOCK_SIZE, USING, WITH PARSER or COMMENT
+     *
+     * @var array
+     */
+    public $options = [];
+
+    /**
+     * CreateIndexDefinitionItem constructor.
+     *
+     * @param Identifier $indexName
+     * @param bool $isPrimary
+     * @param bool $isUnique
+     * @param bool $isSpatial
+     * @param bool $isFulltext
+     */
+    public function __construct(
+        Identifier $indexName = null,
+        bool $isPrimary = false,
+        bool $isUnique = false,
+        bool $isSpatial = false,
+        bool $isFulltext = false
+    ) {
+        $this->indexName = $indexName;
+        $this->isPrimary = $isPrimary;
+        $this->isUnique = $isUnique;
+        $this->isSpatial = $isSpatial;
+        $this->isFulltext = $isFulltext;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateTableClause.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateTableClause.php
new file mode 100644 (file)
index 0000000..62bdedd
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Syntax node to represent the initial CREATE TABLE statement in the
+ * syntax tree. Represents everything up to the start of the definition
+ * of fields/indexes/foreign keys
+ */
+class CreateTableClause
+{
+    /**
+     * @var \TYPO3\CMS\Core\Database\Schema\Parser\AST\Identifier
+     */
+    public $tableName;
+
+    /**
+     * @var bool
+     */
+    public $isTemporary;
+
+    /**
+     * CreateTableClause constructor.
+     *
+     * @param Identifier $tableName
+     * @param bool $isTemporary
+     */
+    public function __construct(Identifier $tableName, bool $isTemporary = false)
+    {
+        $this->tableName = $tableName;
+        $this->isTemporary = $isTemporary;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateTableStatement.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/CreateTableStatement.php
new file mode 100644 (file)
index 0000000..7d74e86
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Root node for a CREATE TABLE statement in the syntax tree.
+ */
+class CreateTableStatement extends AbstractCreateStatement
+{
+    /**
+     * @var \TYPO3\CMS\Core\Database\Schema\Parser\AST\Identifier
+     */
+    public $tableName;
+
+    /**
+     * @var bool
+     */
+    public $isTemporary = false;
+
+    /**
+     * @var \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateDefinition
+     */
+    public $createDefinition;
+
+    /**
+     * @var array
+     */
+    public $tableOptions = [];
+
+    /**
+     * CreateTableStatement constructor.
+     *
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableClause $createTableClause
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateDefinition $createDefinition
+     */
+    public function __construct(CreateTableClause $createTableClause, CreateDefinition $createDefinition)
+    {
+        $this->tableName = $createTableClause->tableName;
+        $this->isTemporary = $createTableClause->isTemporary;
+        $this->createDefinition = $createDefinition;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/AbstractDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/AbstractDataType.php
new file mode 100644 (file)
index 0000000..3fb7d94
--- /dev/null
@@ -0,0 +1,187 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Base class for all data types that contains properties
+ * common to all data types.
+ */
+abstract class AbstractDataType
+{
+    /**
+     * Used by most field types for length/precision information
+     *
+     * @var int
+     */
+    protected $length = 0;
+
+    /**
+     * Used for floating point type columns
+     * -1 is used to indicate that no value has been set.
+     *
+     * @var int
+     */
+    protected $precision = -1;
+
+    /**
+     * Used for floating point type columns
+     * -1 is used to indicate that no value has been set.
+     *
+     * @var int
+     */
+    protected $scale = -1;
+
+    /**
+     * Differentiate between CHAR/VARCHAR and BINARY/VARBINARY
+     *
+     * @var bool
+     */
+    protected $fixed = false;
+
+    /**
+     * Unsigned flag for numeric columns
+     *
+     * @var bool
+     */
+    protected $unsigned = false;
+
+    /**
+     * Extra options for a column that control specific features/flags
+     *
+     * @var array
+     */
+    protected $options = [];
+
+    /**
+     * Options for ENUM/SET data types
+     *
+     * @var array
+     */
+    protected $values;
+
+    /**
+     * @return int
+     */
+    public function getLength(): int
+    {
+        return $this->length;
+    }
+
+    /**
+     * @param int $length
+     */
+    public function setLength(int $length)
+    {
+        $this->length = $length;
+    }
+
+    /**
+     * @return int
+     */
+    public function getPrecision(): int
+    {
+        return $this->precision;
+    }
+
+    /**
+     * @param int $precision
+     */
+    public function setPrecision(int $precision)
+    {
+        $this->precision = $precision;
+    }
+
+    /**
+     * @return int
+     */
+    public function getScale(): int
+    {
+        return $this->scale;
+    }
+
+    /**
+     * @param int $scale
+     */
+    public function setScale(int $scale)
+    {
+        $this->scale = $scale;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isFixed(): bool
+    {
+        return $this->fixed;
+    }
+
+    /**
+     * @param bool $fixed
+     */
+    public function setFixed(bool $fixed)
+    {
+        $this->fixed = $fixed;
+    }
+
+    /**
+     * @return array
+     */
+    public function getOptions(): array
+    {
+        return $this->options;
+    }
+
+    /**
+     * @param array $options
+     */
+    public function setOptions(array $options)
+    {
+        $this->options = $options;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isUnsigned(): bool
+    {
+        return $this->unsigned;
+    }
+
+    /**
+     * @param bool $unsigned
+     */
+    public function setUnsigned(bool $unsigned)
+    {
+        $this->unsigned = $unsigned;
+    }
+
+    /**
+     * @return array
+     */
+    public function getValues(): array
+    {
+        return $this->values;
+    }
+
+    /**
+     * @param array $values
+     */
+    public function setValues(array $values)
+    {
+        $this->values = $values;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/BigIntDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/BigIntDataType.php
new file mode 100644 (file)
index 0000000..8043a97
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the BIGINT SQL column type
+ */
+class BigIntDataType extends IntegerDataType
+{
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/BinaryDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/BinaryDataType.php
new file mode 100644 (file)
index 0000000..61ecb67
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the BINARY SQL column type
+ */
+class BinaryDataType extends AbstractDataType
+{
+    /**
+     * BinaryDataType constructor.
+     *
+     * @param int $length
+     */
+    public function __construct(int $length)
+    {
+        $this->length = $length;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/BitDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/BitDataType.php
new file mode 100644 (file)
index 0000000..a795587
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the BIT SQL column type
+ */
+class BitDataType extends AbstractDataType
+{
+    /**
+     * BitDataType constructor.
+     *
+     * @param int $length
+     */
+    public function __construct(int $length)
+    {
+        $this->length = $length;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/BlobDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/BlobDataType.php
new file mode 100644 (file)
index 0000000..dd2f7f3
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the BLOB SQL column type
+ */
+class BlobDataType extends AbstractDataType
+{
+    /**
+     * BlobDataType constructor.
+     */
+    public function __construct()
+    {
+        // MySQL BLOB can store 64KB
+        $this->length = 65535;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/CharDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/CharDataType.php
new file mode 100644 (file)
index 0000000..223f4be
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the CHAR SQL column type
+ */
+class CharDataType extends AbstractDataType
+{
+    /**
+     * CharDataType constructor.
+     *
+     * @param int $length
+     * @param array $options
+     */
+    public function __construct(int $length, array $options)
+    {
+        $this->length = $length;
+        $this->options = $options;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/DateDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/DateDataType.php
new file mode 100644 (file)
index 0000000..84165b9
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the DATE SQL column type
+ */
+class DateDataType extends AbstractDataType
+{
+    /**
+     * DateDataType constructor.
+     */
+    public function __construct()
+    {
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/DateTimeDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/DateTimeDataType.php
new file mode 100644 (file)
index 0000000..0fef2a6
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the DATETIME SQL column type
+ */
+class DateTimeDataType extends AbstractDataType
+{
+    /**
+     * DateTimeDataType constructor.
+     *
+     * @param int $length
+     */
+    public function __construct(int $length)
+    {
+        $this->length = $length;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/DecimalDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/DecimalDataType.php
new file mode 100644 (file)
index 0000000..620a1f7
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the DECIMAL SQL column type
+ */
+class DecimalDataType extends AbstractDataType
+{
+    /**
+     * DecimalDataType constructor.
+     *
+     * @param array $dataTypeDecimals
+     * @param array $dataTypeOptions
+     */
+    public function __construct(array $dataTypeDecimals, array $dataTypeOptions)
+    {
+        $this->precision = $dataTypeDecimals['length'] ?? -1;
+        $this->scale = $dataTypeDecimals['decimals'] ?? -1;
+        $this->options = $dataTypeOptions;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/DoubleDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/DoubleDataType.php
new file mode 100644 (file)
index 0000000..f1073ad
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the DOUBLE SQL column type
+ */
+class DoubleDataType extends FloatDataType
+{
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/EnumDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/EnumDataType.php
new file mode 100644 (file)
index 0000000..a9b7095
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the ENUM SQL column type
+ */
+class EnumDataType extends AbstractDataType
+{
+    /**
+     * EnumDataType constructor.
+     *
+     * @param array $values
+     * @param array $options
+     */
+    public function __construct(array $values, array $options)
+    {
+        $this->values = $values;
+        $this->options = $options;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/FloatDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/FloatDataType.php
new file mode 100644 (file)
index 0000000..fdd2508
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the FLOAT SQL column type
+ */
+class FloatDataType extends AbstractDataType
+{
+    /**
+     * FloatDataType constructor.
+     *
+     * @param array $dataTypeDecimals
+     * @param array $dataTypeOptions
+     */
+    public function __construct(array $dataTypeDecimals, array $dataTypeOptions)
+    {
+        // -1 is used to indicate that no value has been provided
+        $this->precision = $dataTypeDecimals['length'] ?? -1;
+        $this->scale = $dataTypeDecimals['decimals'] ?? -1;
+        $this->options = $dataTypeOptions;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/IntegerDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/IntegerDataType.php
new file mode 100644 (file)
index 0000000..991c096
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the INT SQL column type
+ */
+class IntegerDataType extends AbstractDataType
+{
+    /**
+     * IntegerDataType constructor.
+     *
+     * @param $length
+     * @param array $options
+     */
+    public function __construct(int $length, array $options)
+    {
+        $this->length = $length;
+        $this->options = $options;
+
+        if (array_key_exists('unsigned', $options) && $options['unsigned']) {
+            $this->setUnsigned(true);
+        }
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/JsonDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/JsonDataType.php
new file mode 100644 (file)
index 0000000..4811d45
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the JSON SQL column type
+ */
+class JsonDataType extends AbstractDataType
+{
+    /**
+     * JsonDataType constructor.
+     */
+    public function __construct()
+    {
+        // JSON is not yet supported by Doctrine 2.5 and will be remapped
+        // to a TEXT type. Setting the length here will ensure a LONGTEXT
+        // column type is selected.
+        $this->length = 2147483647;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/LongBlobDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/LongBlobDataType.php
new file mode 100644 (file)
index 0000000..a29e424
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the LONGBLOB SQL column type
+ */
+class LongBlobDataType extends BlobDataType
+{
+    /**
+     * LongBlobDataType constructor.
+     */
+    public function __construct()
+    {
+        parent::__construct();
+
+        // MySQL LONGBLOB can store 4GB of data, to be 32bit safe only claim 2GB
+        $this->length = 2147483647;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/LongTextDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/LongTextDataType.php
new file mode 100644 (file)
index 0000000..ac9ab3a
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the LONGTEXT SQL column type
+ */
+class LongTextDataType extends TextDataType
+{
+    /**
+     * LongTextDataType constructor.
+     *
+     * @param array $options
+     */
+    public function __construct(array $options)
+    {
+        parent::__construct($options);
+
+        // MySQL LONGTEXT can store 4GB of data, to be 32bit safe only claim 2GB
+        $this->length = 2147483647;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/MediumBlobDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/MediumBlobDataType.php
new file mode 100644 (file)
index 0000000..ecaf20f
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the MEDIUMBLOB SQL column type
+ */
+class MediumBlobDataType extends BlobDataType
+{
+    /**
+     * MediumBlobDataType constructor.
+     */
+    public function __construct()
+    {
+        parent::__construct();
+
+        // MySQL MEDIUMBLOB can store 16MB
+        $this->length = 16777215;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/MediumIntDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/MediumIntDataType.php
new file mode 100644 (file)
index 0000000..aa093f3
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the MEDIUMINT SQL column type
+ */
+class MediumIntDataType extends IntegerDataType
+{
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/MediumTextDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/MediumTextDataType.php
new file mode 100644 (file)
index 0000000..6b2daed
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the MEDIUMTEXT SQL column type
+ */
+class MediumTextDataType extends TextDataType
+{
+    /**
+     * MediumTextDataType constructor.
+     *
+     * @param array $options
+     */
+    public function __construct(array $options)
+    {
+        parent::__construct($options);
+
+        // MySQL MEDIUMTEXT can store 16MB
+        $this->length = 16777215;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/NumericDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/NumericDataType.php
new file mode 100644 (file)
index 0000000..cb799da
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the NUMERIC SQL column type
+ */
+class NumericDataType extends DecimalDataType
+{
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/RealDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/RealDataType.php
new file mode 100644 (file)
index 0000000..4d7b5fe
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the REAL SQL column type
+ */
+class RealDataType extends FloatDataType
+{
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/SetDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/SetDataType.php
new file mode 100644 (file)
index 0000000..ee9ab05
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the SET SQL column type
+ */
+class SetDataType extends AbstractDataType
+{
+    /**
+     * SetDataType constructor.
+     *
+     * @param array $values
+     * @param array $options
+     */
+    public function __construct(array $values, array $options)
+    {
+        $this->values = $values;
+        $this->options = $options;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/SmallIntDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/SmallIntDataType.php
new file mode 100644 (file)
index 0000000..480ffd5
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the SMALLINT SQL column type
+ */
+class SmallIntDataType extends IntegerDataType
+{
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TextDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TextDataType.php
new file mode 100644 (file)
index 0000000..59f2633
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the TEXT SQL column type
+ */
+class TextDataType extends AbstractDataType
+{
+    /**
+     * TextDataType constructor.
+     *
+     * @param array $options
+     */
+    public function __construct(array $options)
+    {
+        // MySQL TEXT can store 64KB
+        $this->length = 65535;
+        $this->options = $options;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TimeDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TimeDataType.php
new file mode 100644 (file)
index 0000000..0308e9d
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the TIME SQL column type
+ */
+class TimeDataType extends AbstractDataType
+{
+    /**
+     * TimeDataType constructor.
+     *
+     * @param int $length
+     */
+    public function __construct(int $length)
+    {
+        $this->length = $length;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TimestampDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TimestampDataType.php
new file mode 100644 (file)
index 0000000..95af406
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the TIMESTAMP SQL column type
+ */
+class TimestampDataType extends AbstractDataType
+{
+    /**
+     * TimestampDataType constructor.
+     *
+     * @param int $length
+     */
+    public function __construct(int $length)
+    {
+        $this->length = $length;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TinyBlobDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TinyBlobDataType.php
new file mode 100644 (file)
index 0000000..9ba1814
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the TINYBLOB SQL column type
+ */
+class TinyBlobDataType extends BlobDataType
+{
+    /**
+     * TinyBlobDataType constructor.
+     */
+    public function __construct()
+    {
+        parent::__construct();
+
+        // MySQL TINYBLOB can store 255 bytes
+        $this->length = 255;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TinyIntDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TinyIntDataType.php
new file mode 100644 (file)
index 0000000..ad688fe
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the TINYINT SQL column type
+ */
+class TinyIntDataType extends IntegerDataType
+{
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TinyTextDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/TinyTextDataType.php
new file mode 100644 (file)
index 0000000..5e3d871
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the TINYTEXT SQL column type
+ */
+class TinyTextDataType extends TextDataType
+{
+    /**
+     * TinyTextDataType constructor.
+     *
+     * @param array $options
+     */
+    public function __construct(array $options)
+    {
+        parent::__construct($options);
+
+        // MySQL TINYTEXT can store 255 characters
+        $this->length = 255;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/VarBinaryDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/VarBinaryDataType.php
new file mode 100644 (file)
index 0000000..c65d6e9
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the VARBINARY SQL column type
+ */
+class VarBinaryDataType extends AbstractDataType
+{
+    /**
+     * VarBinaryDataType constructor.
+     *
+     * @param int $length
+     */
+    public function __construct(int $length)
+    {
+        $this->length = $length;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/VarCharDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/VarCharDataType.php
new file mode 100644 (file)
index 0000000..d62463a
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the VARCHAR SQL column type
+ */
+class VarCharDataType extends AbstractDataType
+{
+    /**
+     * VarCharDataType constructor.
+     *
+     * @param int $length
+     * @param array $options
+     */
+    public function __construct(int $length, array $options)
+    {
+        $this->length = $length;
+        $this->options = $options;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/YearDataType.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/DataType/YearDataType.php
new file mode 100644 (file)
index 0000000..fc6ad22
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Node representing the YEAR SQL column type
+ */
+class YearDataType extends AbstractDataType
+{
+    /**
+     * YearDataType constructor.
+     */
+    public function __construct()
+    {
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/Identifier.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/Identifier.php
new file mode 100644 (file)
index 0000000..8d9602f
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Syntax node to represent identifiers used in various parts of a
+ * SQL statements like table, field or index names.
+ */
+class Identifier
+{
+    /**
+     * @var string
+     */
+    public $schemaObjectName;
+
+    /**
+     * Identifier constructor.
+     *
+     * @param string $schemaObjectName
+     */
+    public function __construct(string $schemaObjectName = null)
+    {
+        $this->schemaObjectName = (string)$schemaObjectName;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/IndexColumnName.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/IndexColumnName.php
new file mode 100644 (file)
index 0000000..01bf62e
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Syntax node to represent a column within an index, which can in MySQL
+ * context consist of the actual column name, length information for a partial
+ * index and a direction which influences default sorting and access patterns.
+ */
+class IndexColumnName
+{
+    /**
+     * @var \TYPO3\CMS\Core\Database\Schema\Parser\AST\Identifier
+     */
+    public $columnName;
+
+    /**
+     * @var int
+     */
+    public $length;
+
+    /**
+     * @var string
+     */
+    public $direction;
+
+    /**
+     * IndexColumnName constructor.
+     *
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\Identifier $columnName
+     * @param int $length
+     * @param string $direction
+     */
+    public function __construct(Identifier $columnName, int $length, string $direction = null)
+    {
+        $this->columnName = $columnName;
+        $this->length = $length;
+        $this->direction = $direction;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/AST/ReferenceDefinition.php b/typo3/sysext/core/Classes/Database/Schema/Parser/AST/ReferenceDefinition.php
new file mode 100644 (file)
index 0000000..dc11652
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser\AST;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Syntax node to represent the REFERENCES part of a foreign key
+ * definition, encapsulating ON UPDATE/ON DELETE actions as well
+ * as the foreign table name and columns.
+ */
+class ReferenceDefinition
+{
+    /**
+     * Match type: FULL, PARTIAL or SIMPLE
+     *
+     * @var string
+     */
+    public $match;
+
+    /**
+     * Reference Option: RESTRICT | CASCADE | SET NULL | NO ACTION
+     *
+     * @var string
+     */
+    public $onDelete;
+
+    /**
+     * Reference Option: RESTRICT | CASCADE | SET NULL | NO ACTION
+     *
+     * @var string
+     */
+    public $onUpdate;
+
+    /**
+     * @var \TYPO3\CMS\Core\Database\Schema\Parser\AST\Identifier
+     */
+    public $tableName;
+
+    /**
+     * @var IndexColumnName[]
+     */
+    public $columnNames;
+
+    /**
+     * ReferenceDefinition constructor.
+     *
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\Identifier $tableName
+     * @param array $columnNames
+     */
+    public function __construct(Identifier $tableName, array $columnNames)
+    {
+        $this->tableName = $tableName;
+        $this->columnNames = $columnNames;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/Lexer.php b/typo3/sysext/core/Classes/Database/Schema/Parser/Lexer.php
new file mode 100644 (file)
index 0000000..9e858a8
--- /dev/null
@@ -0,0 +1,274 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Scans a MySQL CREATE TABLE statement for tokens.
+ */
+class Lexer extends \Doctrine\Common\Lexer
+{
+    // All tokens that are not valid identifiers must be < 100
+    const T_NONE = 1;
+    const T_STRING = 2;
+    const T_INPUT_PARAMETER = 3;
+    const T_CLOSE_PARENTHESIS = 4;
+    const T_OPEN_PARENTHESIS = 5;
+    const T_COMMA = 6;
+    const T_DIVIDE = 7;
+    const T_DOT = 8;
+    const T_EQUALS = 9;
+    const T_GREATER_THAN = 10;
+    const T_LOWER_THAN = 11;
+    const T_MINUS = 12;
+    const T_MULTIPLY = 13;
+    const T_NEGATE = 14;
+    const T_PLUS = 15;
+    const T_OPEN_CURLY_BRACE = 16;
+    const T_CLOSE_CURLY_BRACE = 17;
+    const T_SEMICOLON = 18;
+
+    // All tokens that are identifiers or keywords that could be considered as identifiers should be >= 100
+    const T_IDENTIFIER = 100;
+
+    // All tokens that could be considered as a data type should be >= 200
+    const T_BIT = 201;
+    const T_TINYINT = 202;
+    const T_SMALLINT = 203;
+    const T_MEDIUMINT = 204;
+    const T_INT = 205;
+    const T_INTEGER = 206;
+    const T_BIGINT = 207;
+    const T_REAL = 208;
+    const T_DOUBLE = 209;
+    const T_FLOAT = 210;
+    const T_DECIMAL = 211;
+    const T_NUMERIC = 212;
+    const T_DATE = 213;
+    const T_TIME = 214;
+    const T_TIMESTAMP = 215;
+    const T_DATETIME = 216;
+    const T_YEAR = 217;
+    const T_CHAR = 218;
+    const T_VARCHAR = 219;
+    const T_BINARY = 220;
+    const T_VARBINARY = 221;
+    const T_TINYBLOB = 222;
+    const T_BLOB = 223;
+    const T_MEDIUMBLOB = 224;
+    const T_LONGBLOB = 225;
+    const T_TINYTEXT = 226;
+    const T_TEXT = 227;
+    const T_MEDIUMTEXT = 228;
+    const T_LONGTEXT = 229;
+    const T_ENUM = 230;
+    const T_SET = 231;
+    const T_JSON = 232;
+
+    // All keyword tokens should be >= 300
+    const T_CREATE = 300;
+    const T_TEMPORARY = 301;
+    const T_TABLE = 302;
+    const T_IF = 303;
+    const T_NOT = 304;
+    const T_EXISTS = 305;
+    const T_CONSTRAINT = 306;
+    const T_INDEX = 307;
+    const T_KEY = 308;
+    const T_FULLTEXT = 309;
+    const T_SPATIAL = 310;
+    const T_PRIMARY = 311;
+    const T_UNIQUE = 312;
+    const T_CHECK = 313;
+    const T_DEFAULT = 314;
+    const T_AUTO_INCREMENT = 315;
+    const T_COMMENT = 316;
+    const T_COLUMN_FORMAT = 317;
+    const T_STORAGE = 318;
+    const T_REFERENCES = 319;
+    const T_NULL = 320;
+    const T_FIXED = 321;
+    const T_DYNAMIC = 322;
+    const T_MEMORY = 323;
+    const T_DISK = 324;
+    const T_UNSIGNED = 325;
+    const T_ZEROFILL = 326;
+    const T_CURRENT_TIMESTAMP = 327;
+    const T_CHARACTER = 328;
+    const T_COLLATE = 329;
+    const T_ASC = 330;
+    const T_DESC = 331;
+    const T_MATCH = 332;
+    const T_FULL = 333;
+    const T_PARTIAL = 334;
+    const T_SIMPLE = 335;
+    const T_ON = 336;
+    const T_UPDATE = 337;
+    const T_DELETE = 338;
+    const T_RESTRICT = 339;
+    const T_CASCADE = 340;
+    const T_NO = 341;
+    const T_ACTION = 342;
+    const T_USING = 343;
+    const T_BTREE = 344;
+    const T_HASH = 345;
+    const T_KEY_BLOCK_SIZE = 346;
+    const T_WITH = 347;
+    const T_PARSER = 348;
+    const T_FOREIGN = 349;
+    const T_ENGINE = 350;
+    const T_AVG_ROW_LENGTH = 351;
+    const T_CHECKSUM = 352;
+    const T_COMPRESSION = 353;
+    const T_CONNECTION = 354;
+    const T_DATA= 355;
+    const T_DIRECTORY = 356;
+    const T_DELAY_KEY_WRITE = 357;
+    const T_ENCRYPTION = 358;
+    const T_INSERT_METHOD = 359;
+    const T_MAX_ROWS = 360;
+    const T_MIN_ROWS = 361;
+    const T_PACK_KEYS = 362;
+    const T_PASSWORD = 363;
+    const T_ROW_FORMAT = 364;
+    const T_STATS_AUTO_RECALC = 365;
+    const T_STATS_PERSISTENT = 366;
+    const T_STATS_SAMPLE_PAGES = 367;
+    const T_TABLESPACE = 368;
+    const T_UNION = 369;
+    const T_PRECISION = 370;
+
+    /**
+     * Creates a new statement scanner object.
+     *
+     * @param string $input A statement string.
+     */
+    public function __construct($input)
+    {
+        $this->setInput($input);
+    }
+
+    /**
+     * Lexical catchable patterns.
+     *
+     * @return array
+     */
+    protected function getCatchablePatterns(): array
+    {
+        return [
+            '(?:-?[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', // numbers
+            '`(?:[^`]|``)*`', // quoted identifiers
+            "'(?:[^']|'')*'", // quoted strings
+            '\)', // closing parenthesis
+            '[a-z0-9$_][\w$]*', // unquoted identifiers
+        ];
+    }
+
+    /**
+     * Lexical non-catchable patterns.
+     *
+     * @return array
+     */
+    protected function getNonCatchablePatterns(): array
+    {
+        return ['\s+'];
+    }
+
+    /**
+     * Retrieve token type. Also processes the token value if necessary.
+     *
+     * @param string $value
+     * @return int
+     */
+    protected function getType(&$value): int
+    {
+        $type = self::T_NONE;
+
+        switch (true) {
+            // Recognize numeric values
+            case is_numeric($value):
+                if (strpos($value, '.') !== false || stripos($value, 'e') !== false) {
+                    return self::T_FLOAT;
+                }
+
+                return self::T_INTEGER;
+
+            // Recognize quoted strings
+            case $value[0] === "'":
+                $value = str_replace("''", "'", substr($value, 1, -1));
+
+                return self::T_STRING;
+
+            // Recognize quoted strings
+            case $value[0] === '`':
+                $value = str_replace('``', '`', substr($value, 1, -1));
+
+                return self::T_IDENTIFIER;
+
+            // Recognize identifiers, aliased or qualified names
+            case ctype_alpha($value[0]):
+                $name = 'TYPO3\\CMS\\Core\\Database\\Schema\\Parser\\Lexer::T_' . strtoupper($value);
+
+                if (defined($name)) {
+                    $type = constant($name);
+
+                    if ($type > 100) {
+                        return $type;
+                    }
+                }
+
+                return self::T_STRING;
+
+            // Recognize symbols
+            case $value === '.':
+                return self::T_DOT;
+            case $value === ';':
+                return self::T_SEMICOLON;
+            case $value === ',':
+                return self::T_COMMA;
+            case $value === '(':
+                return self::T_OPEN_PARENTHESIS;
+            case $value === ')':
+                return self::T_CLOSE_PARENTHESIS;
+            case $value === '=':
+                return self::T_EQUALS;
+            case $value === '>':
+                return self::T_GREATER_THAN;
+            case $value === '<':
+                return self::T_LOWER_THAN;
+            case $value === '+':
+                return self::T_PLUS;
+            case $value === '-':
+                return self::T_MINUS;
+            case $value === '*':
+                return self::T_MULTIPLY;
+            case $value === '/':
+                return self::T_DIVIDE;
+            case $value === '!':
+                return self::T_NEGATE;
+            case $value === '{':
+                return self::T_OPEN_CURLY_BRACE;
+            case $value === '}':
+                return self::T_CLOSE_CURLY_BRACE;
+
+            // Default
+            default:
+                // Do nothing
+        }
+
+        return $type;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/Parser.php b/typo3/sysext/core/Classes/Database/Schema/Parser/Parser.php
new file mode 100644 (file)
index 0000000..e4cf537
--- /dev/null
@@ -0,0 +1,1564 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Doctrine\DBAL\Schema\Table;
+use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement;
+
+/**
+ * An LL(*) recursive-descent parser for MySQL CREATE TABLE statements.
+ * Parses a CREATE TABLE statement, reports any errors in it, and generates an AST.
+ */
+class Parser
+{
+    /**
+     * The lexer.
+     *
+     * @var Lexer
+     */
+    protected $lexer;
+
+    /**
+     * The statement to parse.
+     *
+     * @var string
+     */
+    protected $statement;
+
+    /**
+     * Creates a new statement parser object.
+     *
+     * @param string $statement The statement to parse.
+     */
+    public function __construct(string $statement)
+    {
+        $this->statement = $statement;
+        $this->lexer = new Lexer($statement);
+    }
+
+    /**
+     * Gets the lexer used by the parser.
+     *
+     * @return Lexer
+     */
+    public function getLexer(): Lexer
+    {
+        return $this->lexer;
+    }
+
+    /**
+     * Parses and builds AST for the given Query.
+     *
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\AbstractCreateStatement
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    public function getAST(): AST\AbstractCreateStatement
+    {
+        // Parse & build AST
+        return $this->queryLanguage();
+    }
+
+    /**
+     * Attempts to match the given token with the current lookahead token.
+     *
+     * If they match, updates the lookahead token; otherwise raises a syntax
+     * error.
+     *
+     * @param int $token The token type.
+     *
+     * @return void
+     *
+     * @throws StatementException If the tokens don't match.
+     */
+    public function match($token)
+    {
+        $lookaheadType = $this->lexer->lookahead['type'];
+
+        // Short-circuit on first condition, usually types match
+        if ($lookaheadType !== $token) {
+            // If parameter is not identifier (1-99) must be exact match
+            if ($token < Lexer::T_IDENTIFIER) {
+                $this->syntaxError($this->lexer->getLiteral($token));
+            }
+
+            // If parameter is keyword (200+) must be exact match
+            if ($token > Lexer::T_IDENTIFIER) {
+                $this->syntaxError($this->lexer->getLiteral($token));
+            }
+
+            // If parameter is MATCH then FULL, PARTIAL or SIMPLE must follow
+            if ($token === Lexer::T_MATCH
+                && $lookaheadType !== Lexer::T_FULL
+                && $lookaheadType !== Lexer::T_PARTIAL
+                && $lookaheadType !== Lexer::T_SIMPLE
+            ) {
+                $this->syntaxError($this->lexer->getLiteral($token));
+            }
+
+            if ($token === Lexer::T_ON && $lookaheadType !== Lexer::T_DELETE && $lookaheadType !== Lexer::T_UPDATE) {
+                $this->syntaxError($this->lexer->getLiteral($token));
+            }
+        }
+
+        $this->lexer->moveNext();
+    }
+
+    /**
+     * Frees this parser, enabling it to be reused.
+     *
+     * @param bool $deep Whether to clean peek and reset errors.
+     * @param int $position Position to reset.
+     *
+     * @return void
+     */
+    public function free($deep = false, $position = 0)
+    {
+        // WARNING! Use this method with care. It resets the scanner!
+        $this->lexer->resetPosition($position);
+
+        // Deep = true cleans peek and also any previously defined errors
+        if ($deep) {
+            $this->lexer->resetPeek();
+        }
+
+        $this->lexer->token = null;
+        $this->lexer->lookahead = null;
+    }
+
+    /**
+     * Parses a statement string.
+     *
+     * @return Table[]
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \RuntimeException
+     * @throws \InvalidArgumentException
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    public function parse(): array
+    {
+        $ast = $this->getAST();
+
+        if (!$ast instanceof CreateTableStatement) {
+            return [];
+        }
+
+        $tableBuilder = new TableBuilder();
+        $table = $tableBuilder->create($ast);
+
+        return [$table];
+    }
+
+    /**
+     * Generates a new syntax error.
+     *
+     * @param string $expected Expected string.
+     * @param array|null $token Got token.
+     *
+     * @return void
+     *
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    public function syntaxError($expected = '', $token = null)
+    {
+        if ($token === null) {
+            $token = $this->lexer->lookahead;
+        }
+
+        $tokenPos = $token['position'] ?? '-1';
+
+        $message = "line 0, col {$tokenPos}: Error: ";
+        $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected ';
+        $message .= ($this->lexer->lookahead === null) ? 'end of string.' : "'{$token['value']}'";
+
+        throw StatementException::syntaxError($message, StatementException::sqlError($this->statement));
+    }
+
+    /**
+     * Generates a new semantical error.
+     *
+     * @param string $message Optional message.
+     * @param array|null $token Optional token.
+     *
+     * @return void
+     *
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    public function semanticalError($message = '', $token = null)
+    {
+        if ($token === null) {
+            $token = $this->lexer->lookahead;
+        }
+
+        // Minimum exposed chars ahead of token
+        $distance = 12;
+
+        // Find a position of a final word to display in error string
+        $createTableStatement = $this->statement;
+        $length = strlen($createTableStatement);
+        $pos = $token['position'] + $distance;
+        $pos = strpos($createTableStatement, ' ', ($length > $pos) ? $pos : $length);
+        $length = ($pos !== false) ? $pos - $token['position'] : $distance;
+
+        $tokenPos = array_key_exists('position', $token) && $token['position'] > 0 ? $token['position'] : '-1';
+        $tokenStr = substr($createTableStatement, $token['position'], $length);
+
+        // Building informative message
+        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
+
+        throw StatementException::semanticalError($message, StatementException::sqlError($this->statement));
+    }
+
+    /**
+     * Peeks beyond the matched closing parenthesis and returns the first token after that one.
+     *
+     * @param bool $resetPeek Reset peek after finding the closing parenthesis.
+     *
+     * @return array
+     */
+    protected function peekBeyondClosingParenthesis($resetPeek = true)
+    {
+        $token = $this->lexer->peek();
+        $numUnmatched = 1;
+
+        while ($numUnmatched > 0 && $token !== null) {
+            switch ($token['type']) {
+                case Lexer::T_OPEN_PARENTHESIS:
+                    ++$numUnmatched;
+                    break;
+                case Lexer::T_CLOSE_PARENTHESIS:
+                    --$numUnmatched;
+                    break;
+                default:
+                    // Do nothing
+            }
+
+            $token = $this->lexer->peek();
+        }
+
+        if ($resetPeek) {
+            $this->lexer->resetPeek();
+        }
+
+        return $token;
+    }
+
+    /**
+     * queryLanguage ::= CreateTableStatement
+     *
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\AbstractCreateStatement
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    public function queryLanguage(): AST\AbstractCreateStatement
+    {
+        $this->lexer->moveNext();
+
+        if ($this->lexer->lookahead['type'] !== Lexer::T_CREATE) {
+            $this->syntaxError('CREATE');
+        }
+
+        $statement = $this->createStatement();
+
+        // Check for end of string
+        if ($this->lexer->lookahead !== null) {
+            $this->syntaxError('end of string');
+        }
+
+        return $statement;
+    }
+
+    /**
+     * CreateStatement ::= CREATE [TEMPORARY] TABLE
+     * Abstraction to allow for support of other schema objects like views in the future.
+     *
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\AbstractCreateStatement
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    public function createStatement(): AST\AbstractCreateStatement
+    {
+        $statement = null;
+        $this->match(Lexer::T_CREATE);
+
+        switch ($this->lexer->lookahead['type']) {
+            case Lexer::T_TEMPORARY:
+                // Intentional fall-through
+            case Lexer::T_TABLE:
+                $statement = $this->createTableStatement();
+                break;
+            default:
+                $this->syntaxError('TEMPORARY or TABLE');
+                break;
+        }
+
+        $this->match(Lexer::T_SEMICOLON);
+
+        return $statement;
+    }
+
+    /**
+     * CreateTableStatement ::= CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name (create_definition,...) [tbl_options]
+     *
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function createTableStatement(): AST\CreateTableStatement
+    {
+        $createTableStatement = new AST\CreateTableStatement($this->createTableClause(), $this->createDefinition());
+
+        if (!$this->lexer->isNextToken(Lexer::T_SEMICOLON)) {
+            $createTableStatement->tableOptions = $this->tableOptions();
+        }
+        return $createTableStatement;
+    }
+
+    /**
+     * CreateTableClause ::= CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name
+     *
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableClause
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function createTableClause(): AST\CreateTableClause
+    {
+        $isTemporary = false;
+        // Check for TEMPORARY
+        if ($this->lexer->isNextToken(Lexer::T_TEMPORARY)) {
+            $this->match(Lexer::T_TEMPORARY);
+            $isTemporary = true;
+        }
+
+        $this->match(Lexer::T_TABLE);
+
+        // Check for IF NOT EXISTS
+        if ($this->lexer->isNextToken(Lexer::T_IF)) {
+            $this->match(Lexer::T_IF);
+            $this->match(Lexer::T_NOT);
+            $this->match(Lexer::T_EXISTS);
+        }
+
+        // Process schema object name (table name)
+        $tableName = $this->schemaObjectName();
+
+        return new AST\CreateTableClause($tableName, $isTemporary);
+    }
+
+    /**
+     * Parses the table field/index definition
+     *
+     * createDefinition ::= (
+     *  col_name column_definition
+     *  | [CONSTRAINT [symbol]] PRIMARY KEY [index_type] (index_col_name,...) [index_option] ...
+     *  | {INDEX|KEY} [index_name] [index_type] (index_col_name,...) [index_option] ...
+     *  | [CONSTRAINT [symbol]] UNIQUE [INDEX|KEY] [index_name] [index_type] (index_col_name,...) [index_option] ...
+     *  | {FULLTEXT|SPATIAL} [INDEX|KEY] [index_name] (index_col_name,...) [index_option] ...
+     *  | [CONSTRAINT [symbol]] FOREIGN KEY [index_name] (index_col_name,...) reference_definition
+     *  | CHECK (expr)
+     * )
+     *
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateDefinition
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function createDefinition(): AST\CreateDefinition
+    {
+        $createDefinitions = [];
+
+        // Process opening parenthesis
+        $this->match(Lexer::T_OPEN_PARENTHESIS);
+
+        $createDefinitions[] = $this->createDefinitionItem();
+
+        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
+            $this->match(Lexer::T_COMMA);
+
+            // TYPO3 previously accepted invalid SQL files where a create definition
+            // item terminated with a comma before the final closing parenthesis.
+            // Silently swallow the extra comma and stop the create definition parsing.
+            if ($this->lexer->isNextToken(Lexer::T_CLOSE_PARENTHESIS)) {
+                break;
+            }
+
+            $createDefinitions[] = $this->createDefinitionItem();
+        }
+
+        // Process closing parenthesis
+        $this->match(Lexer::T_CLOSE_PARENTHESIS);
+
+        return new AST\CreateDefinition($createDefinitions);
+    }
+
+    /**
+     * Parse the definition of a single column or index
+     *
+     * @see createDefinition()
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\AbstractCreateDefinitionItem
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function createDefinitionItem(): AST\AbstractCreateDefinitionItem
+    {
+        $definitionItem = null;
+
+        switch ($this->lexer->lookahead['type']) {
+            case Lexer::T_FULLTEXT:
+                // Intentional fall-through
+            case Lexer::T_SPATIAL:
+                // Intentional fall-through
+            case Lexer::T_PRIMARY:
+                // Intentional fall-through
+            case Lexer::T_UNIQUE:
+                // Intentional fall-through
+            case Lexer::T_KEY:
+                // Intentional fall-through
+            case Lexer::T_INDEX:
+                $definitionItem = $this->createIndexDefinitionItem();
+                break;
+            case Lexer::T_FOREIGN:
+                $definitionItem = $this->createForeignKeyDefinitionItem();
+                break;
+            case Lexer::T_CONSTRAINT:
+                $this->semanticalError('CONSTRAINT [symbol] index definition part not supported');
+                break;
+            case Lexer::T_CHECK:
+                $this->semanticalError('CHECK (expr) create definition not supported');
+                break;
+            default:
+                $definitionItem = $this->createColumnDefinitionItem();
+        }
+
+        return $definitionItem;
+    }
+
+    /**
+     * Parses an index definition item contained in the create definition
+     *
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateIndexDefinitionItem
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function createIndexDefinitionItem(): AST\CreateIndexDefinitionItem
+    {
+        $indexName = null;
+        $isPrimary = false;
+        $isFulltext = false;
+        $isSpatial = false;
+        $isUnique = false;
+        $indexDefinition = new AST\CreateIndexDefinitionItem();
+
+        switch ($this->lexer->lookahead['type']) {
+            case Lexer::T_PRIMARY:
+                $this->match(Lexer::T_PRIMARY);
+                // KEY is a required keyword for PRIMARY index
+                $this->match(Lexer::T_KEY);
+                $isPrimary = true;
+                break;
+            case Lexer::T_KEY:
+                // Plain index, no special configuration
+                $this->match(Lexer::T_KEY);
+                break;
+            case Lexer::T_INDEX:
+                // Plain index, no special configuration
+                $this->match(Lexer::T_INDEX);
+                break;
+            case Lexer::T_UNIQUE:
+                $this->match(Lexer::T_UNIQUE);
+                // INDEX|KEY are optional keywords for UNIQUE index
+                if ($this->lexer->isNextTokenAny([Lexer::T_INDEX, Lexer::T_KEY])) {
+                    $this->lexer->moveNext();
+                }
+                $isUnique = true;
+                break;
+            case Lexer::T_FULLTEXT:
+                $this->match(Lexer::T_FULLTEXT);
+                // INDEX|KEY are optional keywords for FULLTEXT index
+                if ($this->lexer->isNextTokenAny([Lexer::T_INDEX, Lexer::T_KEY])) {
+                    $this->lexer->moveNext();
+                }
+                $isFulltext = true;
+                break;
+            case Lexer::T_SPATIAL:
+                $this->match(Lexer::T_SPATIAL);
+                // INDEX|KEY are optional keywords for SPATIAL index
+                if ($this->lexer->isNextTokenAny([Lexer::T_INDEX, Lexer::T_KEY])) {
+                    $this->lexer->moveNext();
+                }
+                $isSpatial = true;
+                break;
+            default:
+                $this->syntaxError('PRIMARY, KEY, INDEX, UNIQUE, FULLTEXT or SPATIAL');
+        }
+
+        // PRIMARY KEY has no name in MySQL
+        if (!$indexDefinition->isPrimary) {
+            $indexName = $this->indexName();
+        }
+
+        $indexDefinition = new AST\CreateIndexDefinitionItem(
+            $indexName,
+            $isPrimary,
+            $isUnique,
+            $isSpatial,
+            $isFulltext
+        );
+
+        // FULLTEXT and SPATIAL indexes can not have a type definiton
+        if (!$isFulltext && !$isSpatial) {
+            $indexDefinition->indexType = $this->indexType();
+        }
+
+        $this->match(Lexer::T_OPEN_PARENTHESIS);
+
+        $indexDefinition->columnNames[] = $this->indexColumnName();
+
+        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
+            $this->match(Lexer::T_COMMA);
+            $indexDefinition->columnNames[] = $this->indexColumnName();
+        }
+
+        $this->match(Lexer::T_CLOSE_PARENTHESIS);
+
+        $indexDefinition->options = $this->indexOptions();
+
+        return $indexDefinition;
+    }
+
+    /**
+     * Parses an foreign key definition item contained in the create definition
+     *
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateForeignKeyDefinitionItem
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function createForeignKeyDefinitionItem(): AST\CreateForeignKeyDefinitionItem
+    {
+        $this->match(Lexer::T_FOREIGN);
+        $this->match(Lexer::T_KEY);
+
+        $indexName = $this->indexName();
+
+        $this->match(Lexer::T_OPEN_PARENTHESIS);
+
+        $indexColumns = [];
+        $indexColumns[] = $this->indexColumnName();
+
+        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
+            $this->match(Lexer::T_COMMA);
+            $indexColumns[] = $this->indexColumnName();
+        }
+
+        $this->match(Lexer::T_CLOSE_PARENTHESIS);
+
+        $foreignKeyDefinition = new AST\CreateForeignKeyDefinitionItem(
+            $indexName,
+            $indexColumns,
+            $this->referenceDefinition()
+        );
+
+        return $foreignKeyDefinition;
+    }
+
+    /**
+     * Return the name of an index. No name has been supplied if the next token is USING
+     * which defines the index type.
+     *
+     * @return AST\Identifier
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    public function indexName(): AST\Identifier
+    {
+        $indexName = new AST\Identifier(null);
+        if (!$this->lexer->isNextTokenAny([Lexer::T_USING, Lexer::T_OPEN_PARENTHESIS])) {
+            $indexName = $this->schemaObjectName();
+        }
+
+        return $indexName;
+    }
+
+    /**
+     * IndexType ::= USING { BTREE | HASH }
+     *
+     * @return string
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    public function indexType(): string
+    {
+        $indexType = '';
+        if (!$this->lexer->isNextToken(Lexer::T_USING)) {
+            return $indexType;
+        }
+
+        $this->match(Lexer::T_USING);
+
+        switch ($this->lexer->lookahead['type']) {
+            case Lexer::T_BTREE:
+                $this->match(Lexer::T_BTREE);
+                $indexType = 'BTREE';
+                break;
+            case Lexer::T_HASH:
+                $this->match(Lexer::T_HASH);
+                $indexType = 'HASH';
+                break;
+            default:
+                $this->syntaxError('BTREE or HASH');
+        }
+
+        return $indexType;
+    }
+
+    /**
+     * IndexOptions ::=  KEY_BLOCK_SIZE [=] value
+     *  | index_type
+     *  | WITH PARSER parser_name
+     *  | COMMENT 'string'
+     *
+     * @return array
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    public function indexOptions(): array
+    {
+        $options = [];
+
+        while ($this->lexer->lookahead && !$this->lexer->isNextTokenAny([Lexer::T_COMMA, Lexer::T_CLOSE_PARENTHESIS])) {
+            switch ($this->lexer->lookahead['type']) {
+                case Lexer::T_KEY_BLOCK_SIZE:
+                    $this->match(Lexer::T_KEY_BLOCK_SIZE);
+                    if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
+                        $this->match(Lexer::T_EQUALS);
+                    }
+                    $this->lexer->moveNext();
+                    $options['key_block_size'] = (int)$this->lexer->token['value'];
+                    break;
+                case Lexer::T_USING:
+                    $options['index_type'] = $this->indexType();
+                    break;
+                case Lexer::T_WITH:
+                    $this->match(Lexer::T_WITH);
+                    $this->match(Lexer::T_PARSER);
+                    $options['parser'] = $this->schemaObjectName();
+                    break;
+                case Lexer::T_COMMENT:
+                    $this->match(Lexer::T_COMMENT);
+                    $this->match(Lexer::T_STRING);
+                    $options['comment'] = $this->lexer->token['value'];
+                    break;
+                default:
+                    $this->syntaxError('KEY_BLOCK_SIZE, USING, WITH PARSER or COMMENT');
+            }
+        }
+
+        return $options;
+    }
+
+    /**
+     * CreateColumnDefinitionItem ::= col_name column_definition
+     *
+     * column_definition:
+     *   data_type [NOT NULL | NULL] [DEFAULT default_value]
+     *     [AUTO_INCREMENT] [UNIQUE [KEY] | [PRIMARY] KEY]
+     *     [COMMENT 'string']
+     *     [COLUMN_FORMAT {FIXED|DYNAMIC|DEFAULT}]
+     *     [STORAGE {DISK|MEMORY|DEFAULT}]
+     *     [reference_definition]
+     *
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function createColumnDefinitionItem(): AST\CreateColumnDefinitionItem
+    {
+        $columnName = $this->schemaObjectName();
+        $dataType = $this->columnDataType();
+
+        $columnDefinitionItem = new AST\CreateColumnDefinitionItem($columnName, $dataType);
+
+        while ($this->lexer->lookahead && !$this->lexer->isNextTokenAny([Lexer::T_COMMA, Lexer::T_CLOSE_PARENTHESIS])) {
+            switch ($this->lexer->lookahead['type']) {
+                case Lexer::T_NOT:
+                    $columnDefinitionItem->allowNull = false;
+                    $this->match(Lexer::T_NOT);
+                    $this->match(Lexer::T_NULL);
+                    break;
+                case Lexer::T_NULL:
+                    $columnDefinitionItem->null = true;
+                    $this->match(Lexer::T_NULL);
+                    break;
+                case Lexer::T_DEFAULT:
+                    $columnDefinitionItem->hasDefaultValue = true;
+                    $columnDefinitionItem->defaultValue = $this->columnDefaultValue();
+                    break;
+                case Lexer::T_AUTO_INCREMENT:
+                    $columnDefinitionItem->autoIncrement = true;
+                    $this->match(Lexer::T_AUTO_INCREMENT);
+                    break;
+                case Lexer::T_UNIQUE:
+                    $columnDefinitionItem->unique = true;
+                    $this->match(Lexer::T_UNIQUE);
+                    if ($this->lexer->isNextToken(Lexer::T_KEY)) {
+                        $this->match(Lexer::T_KEY);
+                    }
+                    break;
+                case Lexer::T_PRIMARY:
+                    $columnDefinitionItem->primary = true;
+                    $this->match(Lexer::T_PRIMARY);
+                    if ($this->lexer->isNextToken(Lexer::T_KEY)) {
+                        $this->match(Lexer::T_KEY);
+                    }
+                    break;
+                case Lexer::T_KEY:
+                    $columnDefinitionItem->index = true;
+                    $this->match(Lexer::T_KEY);
+                    break;
+                case Lexer::T_COMMENT:
+                    $this->match(Lexer::T_COMMENT);
+                    if ($this->lexer->isNextToken(Lexer::T_STRING)) {
+                        $columnDefinitionItem->comment = $this->lexer->lookahead['value'];
+                        $this->match(Lexer::T_STRING);
+                    }
+                    break;
+                case Lexer::T_COLUMN_FORMAT:
+                    $this->match(Lexer::T_COLUMN_FORMAT);
+                    if ($this->lexer->isNextToken(Lexer::T_FIXED)) {
+                        $columnDefinitionItem->columnFormat = 'fixed';
+                        $this->match(Lexer::T_FIXED);
+                    } elseif ($this->lexer->isNextToken(Lexer::T_DYNAMIC)) {
+                        $columnDefinitionItem->columnFormat = 'dynamic';
+                        $this->match(Lexer::T_DYNAMIC);
+                    } else {
+                        $this->match(Lexer::T_DEFAULT);
+                    }
+                    break;
+                case Lexer::T_STORAGE:
+                    $this->match(Lexer::T_STORAGE);
+                    if ($this->lexer->isNextToken(Lexer::T_MEMORY)) {
+                        $columnDefinitionItem->storage = 'memory';
+                        $this->match(Lexer::T_MEMORY);
+                    } elseif ($this->lexer->isNextToken(Lexer::T_DISK)) {
+                        $columnDefinitionItem->storage = 'disk';
+                        $this->match(Lexer::T_DISK);
+                    } else {
+                        $this->match(Lexer::T_DEFAULT);
+                    }
+                    break;
+                case Lexer::T_REFERENCES:
+                    $columnDefinitionItem->reference = $this->referenceDefinition();
+                    break;
+                default:
+                    $this->syntaxError(
+                        'NOT, NULL, DEFAULT, AUTO_INCREMENT, UNIQUE, ' .
+                        'PRIMARY, COMMENT, COLUMN_FORMAT, STORAGE or REFERENCES'
+                    );
+            }
+        }
+
+        return $columnDefinitionItem;
+    }
+
+    /**
+     * DataType ::= BIT[(length)]
+     *   | TINYINT[(length)] [UNSIGNED] [ZEROFILL]
+     *   | SMALLINT[(length)] [UNSIGNED] [ZEROFILL]
+     *   | MEDIUMINT[(length)] [UNSIGNED] [ZEROFILL]
+     *   | INT[(length)] [UNSIGNED] [ZEROFILL]
+     *   | INTEGER[(length)] [UNSIGNED] [ZEROFILL]
+     *   | BIGINT[(length)] [UNSIGNED] [ZEROFILL]
+     *   | REAL[(length,decimals)] [UNSIGNED] [ZEROFILL]
+     *   | DOUBLE[(length,decimals)] [UNSIGNED] [ZEROFILL]
+     *   | FLOAT[(length,decimals)] [UNSIGNED] [ZEROFILL]
+     *   | DECIMAL[(length[,decimals])] [UNSIGNED] [ZEROFILL]
+     *   | NUMERIC[(length[,decimals])] [UNSIGNED] [ZEROFILL]
+     *   | DATE
+     *   | TIME[(fsp)]
+     *   | TIMESTAMP[(fsp)]
+     *   | DATETIME[(fsp)]
+     *   | YEAR
+     *   | CHAR[(length)] [BINARY] [CHARACTER SET charset_name] [COLLATE collation_name]
+     *   | VARCHAR(length) [BINARY] [CHARACTER SET charset_name] [COLLATE collation_name]
+     *   | BINARY[(length)]
+     *   | VARBINARY(length)
+     *   | TINYBLOB
+     *   | BLOB
+     *   | MEDIUMBLOB
+     *   | LONGBLOB
+     *   | TINYTEXT [BINARY] [CHARACTER SET charset_name] [COLLATE collation_name]
+     *   | TEXT [BINARY] [CHARACTER SET charset_name] [COLLATE collation_name]
+     *   | MEDIUMTEXT [BINARY] [CHARACTER SET charset_name] [COLLATE collation_name]
+     *   | LONGTEXT [BINARY] [CHARACTER SET charset_name] [COLLATE collation_name]
+     *   | ENUM(value1,value2,value3,...) [CHARACTER SET charset_name] [COLLATE collation_name]
+     *   | SET(value1,value2,value3,...) [CHARACTER SET charset_name] [COLLATE collation_name]
+     *   | JSON
+     *
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\AbstractDataType
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function columnDataType(): AST\DataType\AbstractDataType
+    {
+        $dataType = null;
+
+        switch ($this->lexer->lookahead['type']) {
+            case Lexer::T_BIT:
+                $this->match(Lexer::T_BIT);
+                $dataType = new AST\DataType\BitDataType(
+                    $this->dataTypeLength()
+                );
+                break;
+            case Lexer::T_TINYINT:
+                $this->match(Lexer::T_TINYINT);
+                $dataType = new AST\DataType\TinyIntDataType(
+                    $this->dataTypeLength(),
+                    $this->numericDataTypeOptions()
+                );
+                break;
+            case Lexer::T_SMALLINT:
+                $this->match(Lexer::T_SMALLINT);
+                $dataType = new AST\DataType\SmallIntDataType(
+                    $this->dataTypeLength(),
+                    $this->numericDataTypeOptions()
+                );
+                break;
+            case Lexer::T_MEDIUMINT:
+                $this->match(Lexer::T_MEDIUMINT);
+                $dataType = new AST\DataType\MediumIntDataType(
+                    $this->dataTypeLength(),
+                    $this->numericDataTypeOptions()
+                );
+                break;
+            case Lexer::T_INT:
+                $this->match(Lexer::T_INT);
+                $dataType = new AST\DataType\IntegerDataType(
+                    $this->dataTypeLength(),
+                    $this->numericDataTypeOptions()
+                );
+                break;
+            case Lexer::T_INTEGER:
+                $this->match(Lexer::T_INTEGER);
+                $dataType = new AST\DataType\IntegerDataType(
+                    $this->dataTypeLength(),
+                    $this->numericDataTypeOptions()
+                );
+                break;
+            case Lexer::T_BIGINT:
+                $this->match(Lexer::T_BIGINT);
+                $dataType = new AST\DataType\BigIntDataType(
+                    $this->dataTypeLength(),
+                    $this->numericDataTypeOptions()
+                );
+                break;
+            case Lexer::T_REAL:
+                $this->match(Lexer::T_REAL);
+                $dataType = new AST\DataType\RealDataType(
+                    $this->dataTypeDecimals(),
+                    $this->numericDataTypeOptions()
+                );
+                break;
+            case Lexer::T_DOUBLE:
+                $this->match(Lexer::T_DOUBLE);
+                if ($this->lexer->isNextToken(Lexer::T_PRECISION)) {
+                    $this->match(Lexer::T_PRECISION);
+                }
+                $dataType = new AST\DataType\DoubleDataType(
+                    $this->dataTypeDecimals(),
+                    $this->numericDataTypeOptions()
+                );
+                break;
+            case Lexer::T_FLOAT:
+                $this->match(Lexer::T_FLOAT);
+                $dataType = new AST\DataType\FloatDataType(
+                    $this->dataTypeDecimals(),
+                    $this->numericDataTypeOptions()
+                );
+
+                break;
+            case Lexer::T_DECIMAL:
+                $this->match(Lexer::T_DECIMAL);
+                $dataType = new AST\DataType\DecimalDataType(
+                    $this->dataTypeDecimals(),
+                    $this->numericDataTypeOptions()
+                );
+                break;
+            case Lexer::T_NUMERIC:
+                $this->match(Lexer::T_NUMERIC);
+                $dataType = new AST\DataType\NumericDataType(
+                    $this->dataTypeDecimals(),
+                    $this->numericDataTypeOptions()
+                );
+                break;
+            case Lexer::T_DATE:
+                $this->match(Lexer::T_DATE);
+                $dataType = new AST\DataType\DateDataType();
+                break;
+            case Lexer::T_TIME:
+                $this->match(Lexer::T_TIME);
+                $dataType = new AST\DataType\TimeDataType($this->fractionalSecondsPart());
+                break;
+            case Lexer::T_TIMESTAMP:
+                $this->match(Lexer::T_TIMESTAMP);
+                $dataType = new AST\DataType\TimestampDataType($this->fractionalSecondsPart());
+                break;
+            case Lexer::T_DATETIME:
+                $this->match(Lexer::T_DATETIME);
+                $dataType = new AST\DataType\DateTimeDataType($this->fractionalSecondsPart());
+                break;
+            case Lexer::T_YEAR:
+                $this->match(Lexer::T_YEAR);
+                $dataType = new AST\DataType\YearDataType();
+                break;
+            case Lexer::T_CHAR:
+                $this->match(Lexer::T_CHAR);
+                $dataType = new AST\DataType\CharDataType(
+                    $this->dataTypeLength(),
+                    $this->characterDataTypeOptions()
+                );
+                break;
+            case Lexer::T_VARCHAR:
+                $this->match(Lexer::T_VARCHAR);
+                $dataType = new AST\DataType\VarCharDataType(
+                    $this->dataTypeLength(true),
+                    $this->characterDataTypeOptions()
+                );
+                break;
+            case Lexer::T_BINARY:
+                $this->match(Lexer::T_BINARY);
+                $dataType = new AST\DataType\BinaryDataType($this->dataTypeLength());
+                break;
+            case Lexer::T_VARBINARY:
+                $this->match(Lexer::T_VARBINARY);
+                $dataType = new AST\DataType\VarBinaryDataType($this->dataTypeLength(true));
+                break;
+            case Lexer::T_TINYBLOB:
+                $this->match(Lexer::T_TINYBLOB);
+                $dataType = new AST\DataType\TinyBlobDataType();
+                break;
+            case Lexer::T_BLOB:
+                $this->match(Lexer::T_BLOB);
+                $dataType = new AST\DataType\BlobDataType();
+                break;
+            case Lexer::T_MEDIUMBLOB:
+                $this->match(Lexer::T_MEDIUMBLOB);
+                $dataType = new AST\DataType\MediumBlobDataType();
+                break;
+            case Lexer::T_LONGBLOB:
+                $this->match(Lexer::T_LONGBLOB);
+                $dataType = new AST\DataType\LongBlobDataType();
+                break;
+            case Lexer::T_TINYTEXT:
+                $this->match(Lexer::T_TINYTEXT);
+                $dataType = new AST\DataType\TinyTextDataType($this->characterDataTypeOptions());
+                break;
+            case Lexer::T_TEXT:
+                $this->match(Lexer::T_TEXT);
+                $dataType = new AST\DataType\TextDataType($this->characterDataTypeOptions());
+                break;
+            case Lexer::T_MEDIUMTEXT:
+                $this->match(Lexer::T_MEDIUMTEXT);
+                $dataType = new AST\DataType\MediumTextDataType($this->characterDataTypeOptions());
+                break;
+            case Lexer::T_LONGTEXT:
+                $this->match(Lexer::T_LONGTEXT);
+                $dataType = new AST\DataType\LongTextDataType($this->characterDataTypeOptions());
+                break;
+            case Lexer::T_ENUM:
+                $this->match(Lexer::T_ENUM);
+                $dataType = new AST\DataType\EnumDataType($this->valueList(), $this->enumerationDataTypeOptions());
+                break;
+            case Lexer::T_SET:
+                $this->match(Lexer::T_SET);
+                $dataType = new AST\DataType\SetDataType($this->valueList(), $this->enumerationDataTypeOptions());
+                break;
+            case Lexer::T_JSON:
+                $this->match(Lexer::T_JSON);
+                $dataType = new AST\DataType\JsonDataType();
+                break;
+            default:
+                $this->syntaxError(
+                    'BIT, TINYINT, SMALLINT, MEDIUMINT, INT, INTEGER, BIGINT, REAL, DOUBLE, FLOAT, DECIMAL, NUMERIC, ' .
+                    'DATE, TIME, TIMESTAMP, DATETIME, YEAR, CHAR, VARCHAR, BINARY, VARBINARY, TINYBLOB, BLOB, ' .
+                    'MEDIUMBLOB, LONGBLOB, TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT, ENUM, SET, or JSON'
+                );
+        }
+
+        return $dataType;
+    }
+
+    /**
+     * DefaultValue::= DEFAULT default_value
+     *
+     * @return mixed
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function columnDefaultValue()
+    {
+        $this->match(Lexer::T_DEFAULT);
+        $value = null;
+
+        switch ($this->lexer->lookahead['type']) {
+            case Lexer::T_INTEGER:
+                $value = (int)$this->lexer->lookahead['value'];
+                break;
+            case Lexer::T_FLOAT:
+                $value = (float)$this->lexer->lookahead['value'];
+                break;
+            case Lexer::T_STRING:
+                $value = (string)$this->lexer->lookahead['value'];
+                break;
+            case Lexer::T_CURRENT_TIMESTAMP:
+                $value = 'CURRENT_TIMESTAMP';
+                break;
+            case Lexer::T_NULL:
+                $value = null;
+                break;
+            default:
+                $this->syntaxError('String, Integer, Float, NULL or CURRENT_TIMESTAMP');
+        }
+
+        $this->lexer->moveNext();
+
+        return $value;
+    }
+
+    /**
+     * Determine length parameter of a column field definition, i.E. INT(11) or VARCHAR(255)
+     *
+     * @param bool $required
+     * @return int
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function dataTypeLength(bool $required = false): int
+    {
+        $length = 0;
+        if (!$this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
+            if ($required) {
+                $this->semanticalError('The current data type requires a field length definition.');
+            }
+            return $length;
+        }
+
+        $this->match(Lexer::T_OPEN_PARENTHESIS);
+        $length = (int)$this->lexer->lookahead['value'];
+        $this->match(Lexer::T_INTEGER);
+        $this->match(Lexer::T_CLOSE_PARENTHESIS);
+
+        return $length;
+    }
+
+    /**
+     * Determine length and optional decimal parameter of a column field definition, i.E. DECIMAL(10,6)
+     *
+     * @return array
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    private function dataTypeDecimals(): array
+    {
+        $options = [];
+        if (!$this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
+            return $options;
+        }
+
+        $this->match(Lexer::T_OPEN_PARENTHESIS);
+        $options['length'] = (int)$this->lexer->lookahead['value'];
+        $this->match(Lexer::T_INTEGER);
+
+        if ($this->lexer->isNextToken(Lexer::T_COMMA)) {
+            $this->match(Lexer::T_COMMA);
+            $options['decimals'] = (int)$this->lexer->lookahead['value'];
+            $this->match(Lexer::T_INTEGER);
+        }
+
+        $this->match(Lexer::T_CLOSE_PARENTHESIS);
+
+        return $options;
+    }
+
+    /**
+     * Parse common options for numeric datatypes
+     *
+     * @return array
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function numericDataTypeOptions(): array
+    {
+        $options = ['unsigned' => false, 'zerofill' => false];
+
+        if (!$this->lexer->isNextTokenAny([Lexer::T_UNSIGNED, Lexer::T_ZEROFILL])) {
+            return $options;
+        }
+
+        while ($this->lexer->isNextTokenAny([Lexer::T_UNSIGNED, Lexer::T_ZEROFILL])) {
+            switch ($this->lexer->lookahead['type']) {
+                case Lexer::T_UNSIGNED:
+                    $this->match(Lexer::T_UNSIGNED);
+                    $options['unsigned'] = true;
+                    break;
+                case Lexer::T_ZEROFILL:
+                    $this->match(Lexer::T_ZEROFILL);
+                    $options['zerofill'] = true;
+                    break;
+                default:
+                    $this->syntaxError('USIGNED or ZEROFILL');
+            }
+        }
+
+        return $options;
+    }
+
+    /**
+     * Determine the fractional seconds part support for TIME, DATETIME and TIMESTAMP columns
+     *
+     * @return int
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function fractionalSecondsPart(): int
+    {
+        $fractionalSecondsPart = $this->dataTypeLength();
+        if ($fractionalSecondsPart < 0) {
+            $this->semanticalError('the fractional seconds part for TIME, DATETIME or TIMESTAMP columns must >= 0');
+        }
+        if ($fractionalSecondsPart > 6) {
+            $this->semanticalError('the fractional seconds part for TIME, DATETIME or TIMESTAMP columns must <= 6');
+        }
+
+        return $fractionalSecondsPart;
+    }
+
+    /**
+     * Parse common options for numeric datatypes
+     *
+     * @return array
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function characterDataTypeOptions(): array
+    {
+        $options = ['binary' => false, 'charset' => null, 'collation' => null];
+
+        if (!$this->lexer->isNextTokenAny([Lexer::T_CHARACTER, Lexer::T_COLLATE, Lexer::T_BINARY])) {
+            return $options;
+        }
+
+        while ($this->lexer->isNextTokenAny([Lexer::T_CHARACTER, Lexer::T_COLLATE, Lexer::T_BINARY])) {
+            switch ($this->lexer->lookahead['type']) {
+                case Lexer::T_BINARY:
+                    $this->match(Lexer::T_BINARY);
+                    $options['binary'] = true;
+                    break;
+                case Lexer::T_CHARACTER:
+                    $this->match(Lexer::T_CHARACTER);
+                    $this->match(Lexer::T_SET);
+                    $this->match(Lexer::T_STRING);
+                    $options['charset'] = $this->lexer->token['value'];
+                    break;
+                case Lexer::T_COLLATE:
+                    $this->match(Lexer::T_COLLATE);
+                    $this->match(Lexer::T_STRING);
+                    $options['collation'] = $this->lexer->token['value'];
+                    break;
+                default:
+                    $this->syntaxError('BINARY, CHARACTER SET or COLLATE');
+            }
+        }
+
+        return $options;
+    }
+
+    /**
+     * Parse shared options for enumeration datatypes (ENUM and SET)
+     *
+     * @return array
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function enumerationDataTypeOptions(): array
+    {
+        $options = ['charset' => null, 'collation' => null];
+
+        if (!$this->lexer->isNextTokenAny([Lexer::T_CHARACTER, Lexer::T_COLLATE])) {
+            return $options;
+        }
+
+        while ($this->lexer->isNextTokenAny([Lexer::T_CHARACTER, Lexer::T_COLLATE])) {
+            switch ($this->lexer->lookahead['type']) {
+                case Lexer::T_CHARACTER:
+                    $this->match(Lexer::T_CHARACTER);
+                    $this->match(Lexer::T_SET);
+                    $this->match(Lexer::T_STRING);
+                    $options['charset'] = $this->lexer->token['value'];
+                    break;
+                case Lexer::T_COLLATE:
+                    $this->match(Lexer::T_COLLATE);
+                    $this->match(Lexer::T_STRING);
+                    $options['collation'] = $this->lexer->token['value'];
+                    break;
+                default:
+                    $this->syntaxError('CHARACTER SET or COLLATE');
+            }
+        }
+
+        return $options;
+    }
+
+    /**
+     * Return all defined values for an enumeration datatype (ENUM, SET)
+     *
+     * @return array
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function valueList(): array
+    {
+        $this->match(Lexer::T_OPEN_PARENTHESIS);
+
+        $values = [];
+        $values[] = $this->valueListItem();
+
+        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
+            $this->match(Lexer::T_COMMA);
+            $values[] = $this->valueListItem();
+        }
+
+        $this->match(Lexer::T_CLOSE_PARENTHESIS);
+
+        return $values;
+    }
+
+    /**
+     * Return a value list item for an enumeration set
+     *
+     * @return string
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function valueListItem(): string
+    {
+        $this->match(Lexer::T_STRING);
+
+        return (string)$this->lexer->token['value'];
+    }
+
+    /**
+     * ReferenceDefinition ::= REFERENCES tbl_name (index_col_name,...)
+     *  [MATCH FULL | MATCH PARTIAL | MATCH SIMPLE]
+     *  [ON DELETE reference_option]
+     *  [ON UPDATE reference_option]
+     *
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\ReferenceDefinition
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function referenceDefinition(): AST\ReferenceDefinition
+    {
+        $this->match(Lexer::T_REFERENCES);
+        $tableName = $this->schemaObjectName();
+        $this->match(Lexer::T_OPEN_PARENTHESIS);
+
+        $referenceColumns = [];
+        $referenceColumns[] = $this->indexColumnName();
+
+        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
+            $this->match(Lexer::T_COMMA);
+            $referenceColumns[] = $this->indexColumnName();
+        }
+
+        $this->match(Lexer::T_CLOSE_PARENTHESIS);
+
+        $referenceDefinition = new AST\ReferenceDefinition($tableName, $referenceColumns);
+
+        while (!$this->lexer->isNextTokenAny([Lexer::T_COMMA, Lexer::T_CLOSE_PARENTHESIS])) {
+            switch ($this->lexer->lookahead['type']) {
+                case Lexer::T_MATCH:
+                    $this->match(Lexer::T_MATCH);
+                    $referenceDefinition->match = $this->lexer->lookahead['value'];
+                    $this->lexer->moveNext();
+                    break;
+                case Lexer::T_ON:
+                    $this->match(Lexer::T_ON);
+                    if ($this->lexer->isNextToken(Lexer::T_DELETE)) {
+                        $this->match(Lexer::T_DELETE);
+                        $referenceDefinition->onDelete = $this->referenceOption();
+                    } else {
+                        $this->match(Lexer::T_UPDATE);
+                        $referenceDefinition->onUpdate = $this->referenceOption();
+                    }
+                    break;
+                default:
+                    $this->syntaxError('MATCH, ON DELETE or ON UPDATE');
+            }
+        }
+
+        return $referenceDefinition;
+    }
+
+    /**
+     * IndexColumnName ::= col_name [(length)] [ASC | DESC]
+     *
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\IndexColumnName
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function indexColumnName(): AST\IndexColumnName
+    {
+        $columnName = $this->schemaObjectName();
+        $length = $this->dataTypeLength();
+        $direction = null;
+
+        if ($this->lexer->isNextToken(Lexer::T_ASC)) {
+            $this->match(Lexer::T_ASC);
+            $direction = 'ASC';
+        } elseif ($this->lexer->isNextToken(Lexer::T_DESC)) {
+            $this->match(Lexer::T_DESC);
+            $direction = 'DESC';
+        }
+
+        return new AST\IndexColumnName($columnName, $length, $direction);
+    }
+
+    /**
+     * ReferenceOption ::= RESTRICT | CASCADE | SET NULL | NO ACTION
+     *
+     * @return string
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function referenceOption(): string
+    {
+        $action = null;
+
+        switch ($this->lexer->lookahead['type']) {
+            case Lexer::T_RESTRICT:
+                $this->match(Lexer::T_RESTRICT);
+                $action = 'RESTRICT';
+                break;
+            case Lexer::T_CASCADE:
+                $this->match(Lexer::T_CASCADE);
+                $action = 'CASCADE';
+                break;
+            case Lexer::T_SET:
+                $this->match(Lexer::T_SET);
+                $this->match(Lexer::T_NULL);
+                $action = 'SET NULL';
+                break;
+            case Lexer::T_NO:
+                $this->match(Lexer::T_NO);
+                $this->match(Lexer::T_ACTION);
+                $action = 'NO ACTION';
+                break;
+            default:
+                $this->syntaxError('RESTRICT, CASCADE, SET NULL or NO ACTION');
+        }
+
+        return $action;
+    }
+
+    /**
+     * Parse MySQL table options
+     *
+     *  ENGINE [=] engine_name
+     *  | AUTO_INCREMENT [=] value
+     *  | AVG_ROW_LENGTH [=] value
+     *  | [DEFAULT] CHARACTER SET [=] charset_name
+     *  | CHECKSUM [=] {0 | 1}
+     *  | [DEFAULT] COLLATE [=] collation_name
+     *  | COMMENT [=] 'string'
+     *  | COMPRESSION [=] {'ZLIB'|'LZ4'|'NONE'}
+     *  | CONNECTION [=] 'connect_string'
+     *  | DATA DIRECTORY [=] 'absolute path to directory'
+     *  | DELAY_KEY_WRITE [=] {0 | 1}
+     *  | ENCRYPTION [=] {'Y' | 'N'}
+     *  | INDEX DIRECTORY [=] 'absolute path to directory'
+     *  | INSERT_METHOD [=] { NO | FIRST | LAST }
+     *  | KEY_BLOCK_SIZE [=] value
+     *  | MAX_ROWS [=] value
+     *  | MIN_ROWS [=] value
+     *  | PACK_KEYS [=] {0 | 1 | DEFAULT}
+     *  | PASSWORD [=] 'string'
+     *  | ROW_FORMAT [=] {DEFAULT|DYNAMIC|FIXED|COMPRESSED|REDUNDANT|COMPACT}
+     *  | STATS_AUTO_RECALC [=] {DEFAULT|0|1}
+     *  | STATS_PERSISTENT [=] {DEFAULT|0|1}
+     *  | STATS_SAMPLE_PAGES [=] value
+     *  | TABLESPACE tablespace_name
+     *  | UNION [=] (tbl_name[,tbl_name]...)
+     *
+     * @return array
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function tableOptions(): array
+    {
+        $options = [];
+
+        while ($this->lexer->lookahead && !$this->lexer->isNextToken(Lexer::T_SEMICOLON)) {
+            switch ($this->lexer->lookahead['type']) {
+                case Lexer::T_DEFAULT:
+                    // DEFAULT prefix is optional for COLLATE/CHARACTER SET, do nothing
+                    $this->match(Lexer::T_DEFAULT);
+                    break;
+                case Lexer::T_ENGINE:
+                    $this->match(Lexer::T_ENGINE);
+                    $options['engine'] = (string)$this->tableOptionValue();
+                    break;
+                case Lexer::T_AUTO_INCREMENT:
+                    $this->match(Lexer::T_AUTO_INCREMENT);
+                    $options['auto_increment'] = (int)$this->tableOptionValue();
+                    break;
+                case Lexer::T_AVG_ROW_LENGTH:
+                    $this->match(Lexer::T_AVG_ROW_LENGTH);
+                    $options['average_row_length'] = (int)$this->tableOptionValue();
+                    break;
+                case Lexer::T_CHARACTER:
+                    $this->match(Lexer::T_CHARACTER);
+                    $this->match(Lexer::T_SET);
+                    $options['character_set'] = (string)$this->tableOptionValue();
+                    break;
+                case Lexer::T_CHECKSUM:
+                    $this->match(Lexer::T_CHECKSUM);
+                    $options['checksum'] = (int)$this->tableOptionValue();
+                    break;
+                case Lexer::T_COLLATE:
+                    $this->match(Lexer::T_COLLATE);
+                    $options['collation'] = (string)$this->tableOptionValue();
+                    break;
+                case Lexer::T_COMMENT:
+                    $this->match(Lexer::T_COMMENT);
+                    $options['comment'] = (string)$this->tableOptionValue();
+                    break;
+                case Lexer::T_COMPRESSION:
+                    $this->match(Lexer::T_COMPRESSION);
+                    $options['compression'] = strtoupper((string)$this->tableOptionValue());
+                    if (!in_array($options['compression'], ['ZLIB', 'LZ4', 'NONE'], true)) {
+                        $this->syntaxError('ZLIB, LZ4 or NONE', $this->lexer->token);
+                    }
+                    break;
+                case Lexer::T_CONNECTION:
+                    $this->match(Lexer::T_CONNECTION);
+                    $options['connection'] = (string)$this->tableOptionValue();
+                    break;
+                case Lexer::T_DATA:
+                    $this->match(Lexer::T_DATA);
+                    $this->match(Lexer::T_DIRECTORY);
+                    $options['data_directory'] = (string)$this->tableOptionValue();
+                    break;
+                case Lexer::T_DELAY_KEY_WRITE:
+                    $this->match(Lexer::T_DELAY_KEY_WRITE);
+                    $options['delay_key_write'] = (int)$this->tableOptionValue();
+                    break;
+                case Lexer::T_ENCRYPTION:
+                    $this->match(Lexer::T_ENCRYPTION);
+                    $options['encryption'] = strtoupper((string)$this->tableOptionValue());
+                    if (!in_array($options['encryption'], ['Y', 'N'], true)) {
+                        $this->syntaxError('Y or N', $this->lexer->token);
+                    }
+                    break;
+                case Lexer::T_INDEX:
+                    $this->match(Lexer::T_INDEX);
+                    $this->match(Lexer::T_DIRECTORY);
+                    $options['index_directory'] = (string)$this->tableOptionValue();
+                    break;
+                case Lexer::T_INSERT_METHOD:
+                    $this->match(Lexer::T_INSERT_METHOD);
+                    $options['insert_method'] = strtoupper((string)$this->tableOptionValue());
+                    if (!in_array($options['insert_method'], ['NO', 'FIRST', 'LAST'], true)) {
+                        $this->syntaxError('NO, FIRST or LAST', $this->lexer->token);
+                    }
+                    break;
+                case Lexer::T_KEY_BLOCK_SIZE:
+                    $this->match(Lexer::T_KEY_BLOCK_SIZE);
+                    $options['key_block_size'] = (int)$this->tableOptionValue();
+                    break;
+                case Lexer::T_MAX_ROWS:
+                    $this->match(Lexer::T_MAX_ROWS);
+                    $options['max_rows'] = (int)$this->tableOptionValue();
+                    break;
+                case Lexer::T_MIN_ROWS:
+                    $this->match(Lexer::T_MIN_ROWS);
+                    $options['min_rows'] = (int)$this->tableOptionValue();
+                    break;
+                case Lexer::T_PACK_KEYS:
+                    $this->match(Lexer::T_PACK_KEYS);
+                    $options['pack_keys'] = strtoupper((string)$this->tableOptionValue());
+                    if (!in_array($options['pack_keys'], ['0', '1', 'DEFAULT'], true)) {
+                        $this->syntaxError('0, 1 or DEFAULT', $this->lexer->token);
+                    }
+                    break;
+                case Lexer::T_PASSWORD:
+                    $this->match(Lexer::T_PASSWORD);
+                    $options['password'] = (string)$this->tableOptionValue();
+                    break;
+                case Lexer::T_ROW_FORMAT:
+                    $this->match(Lexer::T_ROW_FORMAT);
+                    $options['row_format'] = (string)$this->tableOptionValue();
+                    $validRowFormats = ['DEFAULT', 'DYNAMIC', 'FIXED', 'COMPRESSED', 'REDUNDANT', 'COMPACT'];
+                    if (!in_array($options['row_format'], $validRowFormats, true)) {
+                        $this->syntaxError(
+                            'DEFAULT, DYNAMIC, FIXED, COMPRESSED, REDUNDANT, COMPACT',
+                            $this->lexer->token
+                        );
+                    }
+                    break;
+                case Lexer::T_STATS_AUTO_RECALC:
+                    $this->match(Lexer::T_STATS_AUTO_RECALC);
+                    $options['stats_auto_recalc'] = strtoupper((string)$this->tableOptionValue());
+                    if (!in_array($options['stats_auto_recalc'], ['0', '1', 'DEFAULT'], true)) {
+                        $this->syntaxError('0, 1 or DEFAULT', $this->lexer->token);
+                    }
+                    break;
+                case Lexer::T_STATS_PERSISTENT:
+                    $this->match(Lexer::T_STATS_PERSISTENT);
+                    $options['stats_persistent'] = strtoupper((string)$this->tableOptionValue());
+                    if (!in_array($options['stats_persistent'], ['0', '1', 'DEFAULT'], true)) {
+                        $this->syntaxError('0, 1 or DEFAULT', $this->lexer->token);
+                    }
+                    break;
+                case Lexer::T_STATS_SAMPLE_PAGES:
+                    $this->match(Lexer::T_STATS_SAMPLE_PAGES);
+                    $options['stats_sample_pages'] = strtoupper((string)$this->tableOptionValue());
+                    if (!in_array($options['stats_sample_pages'], ['0', '1', 'DEFAULT'], true)) {
+                        $this->syntaxError('0, 1 or DEFAULT', $this->lexer->token);
+                    }
+                    break;
+                case Lexer::T_TABLESPACE:
+                    $this->match(Lexer::T_TABLESPACE);
+                    $options['tablespace'] = (string)$this->tableOptionValue();
+                    break;
+                default:
+                    $this->syntaxError(
+                        'DEFAULT, ENGINE, AUTO_INCREMENT, AVG_ROW_LENGTH, CHARACTER SET, ' .
+                        'CHECKSUM, COLLATE, COMMENT, COMPRESSION, CONNECTION, DATA DIRECTORY, ' .
+                        'DELAY_KEY_WRITE, ENCRYPTION, INDEX DIRECTORY, INSERT_METHOD, KEY_BLOCK_SIZE, ' .
+                        'MAX_ROWS, MIN_ROWS, PACK_KEYS, PASSWORD, ROW_FORMAT, STATS_AUTO_RECALC, ' .
+                        'STATS_PERSISTENT, STATS_SAMPLE_PAGES or TABLESPACE'
+                    );
+            }
+        }
+
+        return $options;
+    }
+
+    /**
+     * Return the value of an option, skipping the optional equal sign.
+     *
+     * @return mixed
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function tableOptionValue()
+    {
+        // Skip the optional equals sign
+        if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
+            $this->match(Lexer::T_EQUALS);
+        }
+        $this->lexer->moveNext();
+
+        return $this->lexer->token['value'];
+    }
+
+    /**
+     * Certain objects within MySQL, including database, table, index, column, alias, view, stored procedure,
+     * partition, tablespace, and other object names are known as identifiers.
+     *
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\Identifier
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    protected function schemaObjectName()
+    {
+        $schemaObjectName = $this->lexer->lookahead['value'];
+        $this->lexer->moveNext();
+
+        return new AST\Identifier((string)$schemaObjectName);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/TableBuilder.php b/typo3/sysext/core/Classes/Database/Schema/Parser/TableBuilder.php
new file mode 100644 (file)
index 0000000..fe3cb42
--- /dev/null
@@ -0,0 +1,385 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Database\Schema\Parser;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Doctrine\DBAL\Schema\Column;
+use Doctrine\DBAL\Schema\Index;
+use Doctrine\DBAL\Schema\Table;
+use Doctrine\DBAL\Types\Type;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateForeignKeyDefinitionItem;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateIndexDefinitionItem;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\IndexColumnName;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\ReferenceDefinition;
+use TYPO3\CMS\Core\Database\Schema\Types\EnumType;
+use TYPO3\CMS\Core\Database\Schema\Types\SetType;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Converts a CreateTableStatement syntax node into a Doctrine Table
+ * object that represents the table defined in the original SQL statement.
+ */
+class TableBuilder
+{
+    /**
+     * @var Table
+     */
+    protected $table;
+
+    /**
+     * TableBuilder constructor.
+     */
+    public function __construct()
+    {
+        // Register custom data types as no connection might have
+        // been established yet so the types would not be available
+        // when building tables/columns.
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+
+        foreach ($connectionPool->getCustomDoctrineTypes() as $type => $className) {
+            if (!Type::hasType($type)) {
+                Type::addType($type, $className);
+            }
+        }
+    }
+
+    /**
+     * Create a Doctrine Table object based on the parsed MySQL SQL command.
+     *
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement $tableStatement
+     * @return \Doctrine\DBAL\Schema\Table
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \RuntimeException
+     * @throws \InvalidArgumentException
+     */
+    public function create(CreateTableStatement $tableStatement): Table
+    {
+        $this->table = GeneralUtility::makeInstance(
+            Table::class,
+            $tableStatement->tableName->schemaObjectName,
+            [],
+            [],
+            [],
+            0,
+            $this->buildTableOptions($tableStatement->tableOptions)
+        );
+
+        foreach ($tableStatement->createDefinition->items as $item) {
+            switch (get_class($item)) {
+                case CreateColumnDefinitionItem::class:
+                    $this->addColumn($item);
+                    break;
+                case CreateIndexDefinitionItem::class:
+                    $this->addIndex($item);
+                    break;
+                case CreateForeignKeyDefinitionItem::class:
+                    $this->addForeignKey($item);
+                    break;
+                default:
+                    throw new \RuntimeException(
+                        'Unknown item definition of type "' . get_class($item) . '" encountered.',
+                        1472044085
+                    );
+            }
+        }
+
+        return $this->table;
+    }
+
+    /**
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem $item
+     * @return \Doctrine\DBAL\Schema\Column
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \RuntimeException
+     */
+    protected function addColumn(CreateColumnDefinitionItem $item): Column
+    {
+        $column = $this->table->addColumn(
+            $item->columnName->schemaObjectName,
+            $this->getDoctrineColumnTypeName($item->dataType)
+        );
+
+        $column->setNotnull(!$item->allowNull);
+        $column->setAutoincrement((bool)$item->autoIncrement);
+        $column->setComment($item->comment);
+
+        // Set default value (unless it's an auto increment column)
+        if ($item->hasDefaultValue && !$column->getAutoincrement()) {
+            $column->setDefault($item->defaultValue);
+        }
+
+        if ($item->dataType->getLength()) {
+            $column->setLength($item->dataType->getLength());
+        }
+
+        if ($item->dataType->getPrecision() >= 0) {
+            $column->setPrecision($item->dataType->getPrecision());
+        }
+
+        if ($item->dataType->getScale() >= 0) {
+            $column->setScale($item->dataType->getScale());
+        }
+
+        if ($item->dataType->isUnsigned()) {
+            $column->setUnsigned(true);
+        }
+
+        // Select CHAR/VARCHAR or BINARY/VARBINARY
+        if ($item->dataType->isFixed()) {
+            $column->setFixed(true);
+        }
+
+        if ($item->dataType instanceof DataType\EnumDataType
+            || $item->dataType instanceof DataType\SetDataType
+        ) {
+            $column->setPlatformOption('unquotedValues', $item->dataType->getValues());
+        }
+
+        if ($item->index) {
+            $this->table->addIndex([$item->columnName->schemaObjectName]);
+        }
+
+        if ($item->unique) {
+            $this->table->addUniqueIndex([$item->columnName->schemaObjectName]);
+        }
+
+        if ($item->primary) {
+            $this->table->setPrimaryKey([$item->columnName->schemaObjectName]);
+        }
+
+        if ($item->reference !== null) {
+            $this->addForeignKeyConstraint(
+                [$item->columnName->schemaObjectName],
+                $item->reference
+            );
+        }
+
+        return $column;
+    }
+
+    /**
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateIndexDefinitionItem $item
+     * @return \Doctrine\DBAL\Schema\Index
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     */
+    protected function addIndex(CreateIndexDefinitionItem $item): Index
+    {
+        $indexName = $item->indexName->schemaObjectName;
+
+        $columnNames = array_map(
+            function (IndexColumnName $columnName) {
+                return $columnName->columnName->schemaObjectName;
+            },
+            $item->columnNames
+        );
+
+        if ($item->isPrimary) {
+            $this->table->setPrimaryKey($columnNames);
+        } elseif ($item->isUnique) {
+            $this->table->addUniqueIndex($columnNames, $indexName);
+        } elseif ($item->isFulltext) {
+            $this->table->addIndex($columnNames, $indexName, ['fulltext']);
+        } elseif ($item->isSpatial) {
+            $this->table->addIndex($columnNames, $indexName, ['spatial']);
+        } else {
+            $this->table->addIndex($columnNames, $indexName);
+        }
+
+        $index = $item->isPrimary ? $this->table->getPrimaryKey() : $this->table->getIndex($indexName);
+
+        return $index;
+    }
+
+    /**
+     * Prepare a explicit foreign key definition item to be added to the table being built.
+     *
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateForeignKeyDefinitionItem $item
+     */
+    protected function addForeignKey(CreateForeignKeyDefinitionItem $item)
+    {
+        $indexName = $item->indexName->schemaObjectName ?: null;
+        $localColumnNames = array_map(
+            function (IndexColumnName $columnName) {
+                return $columnName->columnName->schemaObjectName;
+            },
+            $item->columnNames
+        );
+        $this->addForeignKeyConstraint($localColumnNames, $item->reference, $indexName);
+    }
+
+    /**
+     * Add a foreign key constraint to the table being built.
+     *
+     * @param string[] $localColumnNames
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\ReferenceDefinition $referenceDefinition
+     * @param string $indexName
+     */
+    protected function addForeignKeyConstraint(
+        array $localColumnNames,
+        ReferenceDefinition $referenceDefinition,
+        string $indexName = null
+    ) {
+        $foreignTableName = $referenceDefinition->tableName->schemaObjectName;
+        $foreignColumNames = array_map(
+            function (IndexColumnName $columnName) {
+                return $columnName->columnName->schemaObjectName;
+            },
+            $referenceDefinition->columnNames
+        );
+
+        $options = [
+            'onDelete' => $referenceDefinition->onDelete,
+            'onUpdate' => $referenceDefinition->onUpdate,
+        ];
+
+        $this->table->addForeignKeyConstraint(
+            $foreignTableName,
+            $localColumnNames,
+            $foreignColumNames,
+            $options,
+            $indexName
+        );
+    }
+
+    /**
+     * @param \TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\AbstractDataType $dataType
+     * @return string
+     * @throws \RuntimeException
+     */
+    protected function getDoctrineColumnTypeName(DataType\AbstractDataType $dataType): string
+    {
+        $doctrineType = null;
+        switch (get_class($dataType)) {
+            case DataType\TinyIntDataType::class:
+                // TINYINT is MySQL specific and mapped to a standard SMALLINT
+            case DataType\SmallIntDataType::class:
+                $doctrineType = Type::SMALLINT;
+                break;
+            case DataType\MediumIntDataType::class:
+                // MEDIUMINT is MySQL specific and mapped to a standard INT
+            case DataType\IntegerDataType::class:
+                $doctrineType = Type::INTEGER;
+                break;
+            case DataType\BigIntDataType::class:
+                $doctrineType = Type::BIGINT;
+                break;
+            case DataType\BinaryDataType::class:
+            case DataType\VarBinaryDataType::class:
+                // CHAR/VARCHAR is determined by "fixed" column property
+                $doctrineType = Type::BINARY;
+                break;
+            case DataType\TinyBlobDataType::class:
+            case DataType\MediumBlobDataType::class:
+            case DataType\BlobDataType::class:
+            case DataType\LongBlobDataType::class:
+                // Actual field type is determined by field length
+                $doctrineType = Type::BLOB;
+                break;
+            case DataType\DateDataType::class:
+                $doctrineType = Type::DATE;
+                break;
+            case DataType\TimestampDataType::class:
+            case DataType\DateTimeDataType::class:
+                // TIMESTAMP or DATETIME are determined by "version" column property
+                $doctrineType = Type::DATETIME;
+                break;
+            case DataType\NumericDataType::class:
+            case DataType\DecimalDataType::class:
+                $doctrineType = Type::DECIMAL;
+                break;
+            case DataType\RealDataType::class:
+            case DataType\FloatDataType::class:
+            case DataType\DoubleDataType::class:
+                $doctrineType = Type::FLOAT;
+                break;
+            case DataType\TimeDataType::class:
+                $doctrineType = Type::TIME;
+                break;
+            case DataType\TinyTextDataType::class:
+            case DataType\MediumTextDataType::class:
+            case DataType\TextDataType::class:
+            case DataType\LongTextDataType::class:
+                $doctrineType = Type::TEXT;
+                break;
+            case DataType\CharDataType::class:
+            case DataType\VarCharDataType::class:
+                $doctrineType = Type::STRING;
+                break;
+            case DataType\EnumDataType::class:
+                $doctrineType = EnumType::TYPE;
+                break;
+            case DataType\SetDataType::class:
+                $doctrineType = SetType::TYPE;
+                break;
+            case DataType\JsonDataType::class:
+                // JSON is not supported in Doctrine 2.5, mapping to the more generic TEXT type
+                $doctrineType = SetType::TEXT;
+                break;
+            case DataType\YearDataType::class:
+                // The YEAR data type is MySQL specific and offers little to no benefit.
+                // The two-digit year logic implemented in this data type (1-69 mapped to
+                // 2001-2069, 70-99 mapped to 1970-1999) can be easily implemented in the
+                // application and for all other accounts it's an integer with a valid
+                // range of 1901 to 2155.
+                // Using a SMALLINT covers the value range and ensures database compatibility.
+                $doctrineType = SetType::SMALLINT;
+                break;
+            default:
+                throw new \RuntimeException(
+                    'Unsupported data type: ' . get_class($dataType) . '!',
+                    1472046376
+                );
+        }
+
+        return $doctrineType;
+    }
+
+    /**
+     * Build the table specific options as far as they are supported by Doctrine.
+     *
+     * @param array $tableOptions
+     * @return array
+     */
+    protected function buildTableOptions(array $tableOptions): array
+    {
+        $options = [];
+
+        if (!empty($tableOptions['engine'])) {
+            $options['engine'] = (string)$tableOptions['engine'];
+        }
+        if (!empty($tableOptions['character_set'])) {
+            $options['charset'] = (string)$tableOptions['character_set'];
+        }
+        if (!empty($tableOptions['collation'])) {
+            $options['collate'] = (string)$tableOptions['collation'];
+        }
+        if (!empty($tableOptions['auto_increment'])) {
+            $options['auto_increment'] = (string)$tableOptions['auto_increment'];
+        }
+        if (!empty($tableOptions['comment'])) {
+            $options['comment'] = (string)$tableOptions['comment'];
+        }
+        if (!empty($tableOptions['row_format'])) {
+            $options['row_format'] = (string)$tableOptions['row_format'];
+        }
+
+        return $options;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/SchemaMigrator.php b/typo3/sysext/core/Classes/Database/Schema/SchemaMigrator.php
new file mode 100644 (file)
index 0000000..b9087f8
--- /dev/null
@@ -0,0 +1,1082 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Core\Database\Schema;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Doctrine\DBAL\DBALException;
+use Doctrine\DBAL\Schema\Column;
+use Doctrine\DBAL\Schema\ColumnDiff;
+use Doctrine\DBAL\Schema\Comparator;
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\DBAL\Schema\SchemaConfig;
+use Doctrine\DBAL\Schema\SchemaDiff;
+use Doctrine\DBAL\Schema\Table;
+use Doctrine\DBAL\Schema\TableDiff;
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Schema\Parser\Parser;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\StringUtility;
+
+/**
+ * Helper methods to handle SQL files and transform them into individual statements
+ * for further processing.
+ *
+ * @internal
+ */
+class SchemaMigrator
+{
+    /**
+     * @var string Prefix of deleted tables
+     */
+    protected $deletedPrefix = 'zzz_deleted_';
+
+    /**
+     * @var Table[]
+     */
+    protected $tables = [];
+
+    /**
+     * @var Schema[]
+     */
+    protected $schema = [];
+
+    /**
+     * Compare current and expected schema definitions and provide updates suggestions in the form
+     * of SQL statements.
+     *
+     * @param string[] $statements The CREATE TABLE statements
+     * @param bool $remove TRUE for RENAME/DROP table and column suggestions, FALSE for ADD/CHANGE suggestions
+     * @return array[] SQL statements to migrate the database to the expected schema, indexed by performed operation
+     * @throws \Doctrine\DBAL\DBALException
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \InvalidArgumentException
+     * @throws \RuntimeException
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    public function getUpdateSuggestions(array $statements, bool $remove = false): array
+    {
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $this->parseCreateTableStatements($statements);
+
+        $updateSuggestions = [];
+
+        foreach ($connectionPool->getConnectionNames() as $connectionName) {
+            $connection = $connectionPool->getConnectionByName($connectionName);
+
+            $schemaDiff = $this->buildSchemaDiff($connectionName);
+
+            if ($remove === false) {
+                $updateSuggestions[$connectionName] = array_merge_recursive(
+                    ['add' => [], 'create_table' => [], 'change' => [], 'change_currentValue' => []],
+                    $this->getNewFieldUpdateSuggestions($schemaDiff, $connection),
+                    $this->getNewTableUpdateSuggestions($schemaDiff, $connection),
+                    $this->getChangedFieldUpdateSuggestions($schemaDiff, $connection)
+                );
+            } else {
+                $updateSuggestions[$connectionName] = array_merge_recursive(
+                    ['change' => [], 'change_table' => [], 'drop' => [], 'drop_table' => [], 'tables_count' => []],
+                    $this->getUnusedFieldUpdateSuggestions($schemaDiff, $connection),
+                    $this->getUnusedTableUpdateSuggestions($schemaDiff, $connection),
+                    $this->getDropTableUpdateSuggestions($schemaDiff, $connection),
+                    $this->getDropFieldUpdateSuggestions($schemaDiff, $connection)
+                );
+            }
+        }
+
+        return $updateSuggestions;
+    }
+
+    /**
+     * Return the raw Doctrine SchemaDiff objects for each connection. This diff contains
+     * all changes without any pre-processing.
+     *
+     * @param array $statements
+     * @return SchemaDiff[]
+     * @throws \Doctrine\DBAL\DBALException
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \InvalidArgumentException
+     * @throws \RuntimeException
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    public function getSchemaDiffs(array $statements): array
+    {
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $this->parseCreateTableStatements($statements);
+
+        $schemaDiffs = [];
+
+        foreach ($connectionPool->getConnectionNames() as $connectionName) {
+            $schemaDiffs[$connectionName] = $this->buildSchemaDiff($connectionName, false);
+        }
+
+        return $schemaDiffs;
+    }
+
+    /**
+     * This method executes statements from the update suggestions, or a subset of them
+     * filtered by the statements hashes, one by one.
+     *
+     * @param string[] $statements The CREATE TABLE statements
+     * @param string[] $selectedStatements The hashes of the update suggestions to execute
+     * @return array
+     * @throws \Doctrine\DBAL\DBALException
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \InvalidArgumentException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     * @throws \RuntimeException
+     */
+    public function migrate(array $statements, array $selectedStatements): array
+    {
+        $result = [];
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $updateSuggestionsPerConnection = array_merge_recursive(
+            $this->getUpdateSuggestions($statements),
+            $this->getUpdateSuggestions($statements, true)
+        );
+
+        foreach ($updateSuggestionsPerConnection as $connectionName => $updateSuggestions) {
+            unset($updateSuggestions['tables_count'], $updateSuggestions['change_currentValue']);
+            $updateSuggestions = array_merge(...array_values($updateSuggestions));
+            $statementsToExecute = array_intersect_key($updateSuggestions, $selectedStatements);
+            if (count($statementsToExecute) === 0) {
+                continue;
+            }
+
+            $connection = $connectionPool->getConnectionByName($connectionName);
+            foreach ($statementsToExecute as $hash => $statement) {
+                try {
+                    $connection->executeUpdate($statement);
+                } catch (DBALException $e) {
+                    $result[$hash] = $e->getPrevious()->getMessage();
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Perform add/change/create operations on tables and fields in an optimized,
+     * non-interactive, mode using the original doctrine SchemaManager ->toSaveSql()
+     * method.
+     *
+     * @param string[] $statements The CREATE TABLE statements
+     * @param bool $createOnly Only perform changes that add fields or create tables
+     * @return array[] Error messages for statements that occured during the installation procedure.
+     * @throws \Doctrine\DBAL\DBALException
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \InvalidArgumentException
+     * @throws \RuntimeException
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     */
+    public function install(array $statements, bool $createOnly = false): array
+    {
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $this->parseCreateTableStatements($statements);
+        $result = [];
+
+        foreach ($connectionPool->getConnectionNames() as $connectionName) {
+            $connection = $connectionPool->getConnectionByName($connectionName);
+
+            $schemaDiff = $this->buildSchemaDiff($connectionName, false);
+
+            $schemaDiff->removedTables = [];
+            foreach ($schemaDiff->changedTables as $key => $changedTable) {
+                $schemaDiff->changedTables[$key]->removedColumns = [];
+                $schemaDiff->changedTables[$key]->removedIndexes = [];
+                if ($createOnly) {
+                    $schemaDiff->changedTables[$key]->changedColumns = [];
+                    $schemaDiff->changedTables[$key]->renamedIndexes = [];
+                }
+            }
+
+            $statements = $schemaDiff->toSaveSql($connection->getDatabasePlatform());
+
+            foreach ($statements as $hash => $statement) {
+                try {
+                    $connection->executeUpdate($statement);
+                    $result[$statement] = '';
+                } catch (DBALException $e) {
+                    $result[$statement] = $e->getPrevious()->getMessage();
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Import static data (INSERT statements)
+     *
+     * @param array $statements
+     * @param bool $truncate
+     * @return array
+     */
+    public function importStaticData(array $statements, bool $truncate = false): array
+    {
+        $result = [];
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $insertStatements = [];
+
+        foreach ($statements as $statement) {
+            // Only handle insert statements and extract the table at the same time. Extracting
+            // the table name is required to perform the inserts on the right connection.
+            if (preg_match('/^INSERT\s+INTO\s+`?(\w+)`?(.*)/i', $statement, $matches)) {
+                list(, $tableName, $sqlFragment) = $matches;
+                $insertStatements[$tableName][] = sprintf(
+                    'INSERT INTO %s %s',
+                    $connectionPool->getConnectionForTable($tableName)->quoteIdentifier($tableName),
+                    rtrim($sqlFragment, ';')
+                );
+            }
+        }
+
+        foreach ($insertStatements as $tableName => $perTableStatements) {
+            $connection = $connectionPool->getConnectionForTable($tableName);
+
+            if ($truncate) {
+                $connection->truncate($tableName);
+            }
+
+            foreach ((array)$perTableStatements as $statement) {
+                try {
+                    $connection->executeUpdate($statement);
+                    $result[$statement] = '';
+                } catch (DBALException $e) {
+                    $result[$statement] = $e->getPrevious()->getMessage();
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * If the schema is not for the Default connection remove all tables from the schema
+     * that have no mapping in the TYPO3 configuration. This avoids update suggestions
+     * for tables that are in the database but have no direct relation to the TYPO3 instance.
+     *
+     * @param string $connectionName
+     * @param bool $renameUnused
+     * @return \Doctrine\DBAL\Schema\SchemaDiff
+     * @throws \Doctrine\DBAL\DBALException
+     * @throws \InvalidArgumentException
+     */
+    protected function buildSchemaDiff(string $connectionName, bool $renameUnused = true): SchemaDiff
+    {
+        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionByName($connectionName);
+
+        // Build the schema definitions
+        $fromSchema = $connection->getSchemaManager()->createSchema();
+        $toSchema = $this->buildExpectedSchemaDefinitions($connectionName);
+
+        // Build SchemaDiff and handle renames of tables and colums
+        $comparator = GeneralUtility::makeInstance(Comparator::class);
+        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
+
+        if ($renameUnused) {
+            $schemaDiff = $this->migrateUnprefixedRemovedTablesToRenames($schemaDiff);
+            $schemaDiff = $this->migrateUnprefixedRemovedFieldsToRenames($schemaDiff);
+        }
+
+        // All tables in the default connection are managed by TYPO3
+        if ($connectionName === ConnectionPool::DEFAULT_CONNECTION_NAME) {
+            return $schemaDiff;
+        }
+
+        // If there are no mapped tables return a SchemaDiff without any changes
+        // to avoid update suggestions for tables not related to TYPO3.
+        if (empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
+            || !is_array($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
+        ) {
+            $schemaDiff = GeneralUtility::makeInstance(SchemaDiff::class, [], [], [], $fromSchema);
+
+            return $schemaDiff;
+        }
+
+        // Collect the table names that have been mapped to this connection.
+        $tablesForConnection = array_keys(
+            array_filter(
+                $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'],
+                function ($tableConnectionName) use ($connectionName) {
+                    return $tableConnectionName === $connectionName;
+                }
+            )
+        );
+
+        // Remove all tables that are not assigned to this connection from the diff
+        $schemaDiff->newTables = $this->removeUnrelatedTables($schemaDiff->newTables, $tablesForConnection);
+        $schemaDiff->changedTables = $this->removeUnrelatedTables($schemaDiff->changedTables, $tablesForConnection);
+        $schemaDiff->removedTables = $this->removeUnrelatedTables($schemaDiff->removedTables, $tablesForConnection);
+
+        return $schemaDiff;
+    }
+
+    /**
+     * Build the expected schema definitons from raw SQL statements.
+     *
+     * @param string $connectionName
+     * @return \Doctrine\DBAL\Schema\Schema
+     * @throws \Doctrine\DBAL\DBALException
+     * @throws \InvalidArgumentException
+     */
+    protected function buildExpectedSchemaDefinitions(string $connectionName): Schema
+    {
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $connection = $connectionPool->getConnectionByName($connectionName);
+
+        $tablesForConnection = [];
+        foreach ($this->tables as $table) {
+            $tableName = $table->getName();
+
+            // Skip tables for a different connection
+            if ($connectionName !== $this->getConnectionNameForTable($tableName)) {
+                continue;
+            }
+
+            if (!array_key_exists($tableName, $tablesForConnection)) {
+                $tablesForConnection[$tableName] = $table;
+                continue;
+            }
+
+            // Merge multiple table definitions. Later definitions overrule identical
+            // columns, indexes and foreign_keys. Order of definitions is based on
+            // extension load order.
+            $currentTableDefinition = $tablesForConnection[$tableName];
+            $tablesForConnection[$tableName] = GeneralUtility::makeInstance(
+                Table::class,
+                $tableName,
+                array_merge($currentTableDefinition->getColumns(), $table->getColumns()),
+                array_merge($currentTableDefinition->getIndexes(), $table->getIndexes()),
+                array_merge($currentTableDefinition->getForeignKeys(), $table->getForeignKeys()),
+                0,
+                array_merge($currentTableDefinition->getOptions(), $table->getOptions())
+            );
+        }
+
+        $schemaConfig = GeneralUtility::makeInstance(SchemaConfig::class);
+        $schemaConfig->setName($connection->getDatabase());
+
+        return GeneralUtility::makeInstance(Schema::class, $tablesForConnection, [], $schemaConfig);
+    }
+
+    /**
+     * Parse CREATE TABLE statements into Doctrine Table objects.
+     *
+     * @param string[] $statements The SQL CREATE TABLE statements
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \InvalidArgumentException
+     * @throws \RuntimeException
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
+     */
+    protected function parseCreateTableStatements(array $statements)
+    {
+        $tables = [];
+        foreach ($statements as $statement) {
+            $createTableParser = GeneralUtility::makeInstance(Parser::class, $statement);
+
+            // We need to keep multiple table definitions at this point so
+            // that Extensions can modify existing tables.
+            $tables[] = $createTableParser->parse();
+        }
+
+        // Flatten the array of arrays by one level
+        $this->tables = array_merge(...$tables);
+    }
+
+    /**
+     * Extract the update suggestions (SQL statements) for newly added tables
+     * from the complete schema diff.
+     *
+     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
+     * @param \TYPO3\CMS\Core\Database\Connection $connection
+     * @return array
+     * @throws \InvalidArgumentException
+     */
+    protected function getNewTableUpdateSuggestions(SchemaDiff $schemaDiff, Connection $connection): array
+    {
+        // Build a new schema diff that only contains added tables
+        $addTableSchemaDiff = GeneralUtility::makeInstance(
+            SchemaDiff::class,
+            $schemaDiff->newTables,
+            [],
+            [],
+            $schemaDiff->fromSchema
+        );
+
+        $statements = $addTableSchemaDiff->toSql($connection->getDatabasePlatform());
+
+        return ['create_table' => $this->calculateUpdateSuggestionsHashes($statements)];
+    }
+
+    /**
+     * Extract the update suggestions (SQL statements) for newly added fields
+     * from the complete schema diff.
+     *
+     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
+     * @param \TYPO3\CMS\Core\Database\Connection $connection
+     * @return array
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \InvalidArgumentException
+     */
+    protected function getNewFieldUpdateSuggestions(SchemaDiff $schemaDiff, Connection $connection): array
+    {
+        $changedTables = [];
+
+        foreach ($schemaDiff->changedTables as $index => $changedTable) {
+            if (count($changedTable->addedColumns) !== 0) {
+                // Treat each added column with a new diff to get a dedicated suggestions
+                // just for this single column.
+                foreach ($changedTable->addedColumns as $addedColumn) {
+                    $changedTables[$index . ':tbl_' . $addedColumn->getName()] = GeneralUtility::makeInstance(
+                        TableDiff::class,
+                        $changedTable->name,
+                        [$addedColumn],
+                        [],
+                        [],
+                        [],
+                        [],
+                        [],
+                        $schemaDiff->fromSchema->getTable($changedTable->name)
+                    );
+                }
+            }
+
+            if (count($changedTable->addedIndexes) !== 0) {
+                // Treat each added index with a new diff to get a dedicated suggestions
+                // just for this index.
+                foreach ($changedTable->addedIndexes as $addedIndex) {
+                    $changedTables[$index . ':idx_' . $addedIndex->getName()] = GeneralUtility::makeInstance(
+                        TableDiff::class,
+                        $changedTable->name,
+                        [],
+                        [],
+                        [],
+                        [$addedIndex],
+                        [],
+                        [],
+                        $schemaDiff->fromSchema->getTable($changedTable->name)
+                    );
+                }
+            }
+
+            if (count($changedTable->addedForeignKeys) !== 0) {
+                // Treat each added foreign key with a new diff to get a dedicated suggestions
+                // just for this foreign key.
+                foreach ($changedTable->addedForeignKeys as $addedForeignKey) {
+                    $fkIndex = $index . ':fk_' . $addedForeignKey->getName();
+                    $changedTables[$fkIndex] = GeneralUtility::makeInstance(
+                        TableDiff::class,
+                        $changedTable->name,
+                        [],
+                        [],
+                        [],
+                        [],
+                        [],
+                        [],
+                        $schemaDiff->fromSchema->getTable($changedTable->name)
+                    );
+                    $changedTables[$fkIndex]->addedForeignKeys = [$addedForeignKey];
+                }
+            }
+        }
+
+        // Build a new schema diff that only contains added fields
+        $addFieldSchemaDiff = GeneralUtility::makeInstance(
+            SchemaDiff::class,
+            [],
+            $changedTables,
+            [],
+            $schemaDiff->fromSchema
+        );
+
+        $statements = $addFieldSchemaDiff->toSql($connection->getDatabasePlatform());
+
+        return ['add' => $this->calculateUpdateSuggestionsHashes($statements)];
+    }
+
+    /**
+     * Extract update suggestions (SQL statements) for changed fields
+     * from the complete schema diff.
+     *
+     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
+     * @param \TYPO3\CMS\Core\Database\Connection $connection
+     * @return array
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \InvalidArgumentException
+     */
+    protected function getChangedFieldUpdateSuggestions(SchemaDiff $schemaDiff, Connection $connection): array
+    {
+        $databasePlatform = $connection->getDatabasePlatform();
+        $updateSuggestions = [];
+
+        foreach ($schemaDiff->changedTables as $index => $changedTable) {
+            if (count($changedTable->changedColumns) !== 0) {
+                // Treat each changed column with a new diff to get a dedicated suggestions
+                // just for this single column.
+                $fromTable = $schemaDiff->fromSchema->getTable($changedTable->name);
+                foreach ($changedTable->changedColumns as $changedColumn) {
+                    // Field has been renamed and will be handled separately
+                    if ($changedColumn->getOldColumnName()->getName() !== $changedColumn->column->getName()) {
+                        continue;
+                    }
+
+                    // Get the current SQL declaration for the column
+                    $currentColumn = $fromTable->getColumn($changedColumn->getOldColumnName()->getName());
+                    $currentDeclaration = $databasePlatform->getColumnDeclarationSQL(
+                        $currentColumn->getName(),
+                        $currentColumn->toArray()
+                    );
+
+                    // Build a dedicated diff just for the current column
+                    $tableDiff = GeneralUtility::makeInstance(
+                        TableDiff::class,
+                        $changedTable->name,
+                        [],
+                        [$changedColumn],
+                        [],
+                        [],
+                        [],
+                        [],
+                        $schemaDiff->fromSchema->getTable($changedTable->name)
+                    );
+
+                    $temporarySchemaDiff = GeneralUtility::makeInstance(
+                        SchemaDiff::class,
+                        [],
+                        [$tableDiff],
+                        [],
+                        $schemaDiff->fromSchema
+                    );
+
+                    $statements = $temporarySchemaDiff->toSql($databasePlatform);
+                    foreach ($statements as $statement) {
+                        $updateSuggestions['change'][md5($statement)] = $statement;
+                        $updateSuggestions['change_currentValue'][md5($statement)] = $currentDeclaration;
+                    }
+                }
+            }
+
+            // Treat each changed index with a new diff to get a dedicated suggestions
+            // just for this index.
+            if (count($changedTable->changedIndexes) !== 0) {
+                foreach ($changedTable->renamedIndexes as $key => $changedIndex) {
+                    $indexDiff = GeneralUtility::makeInstance(
+                        TableDiff::class,
+                        $changedTable->name,
+                        [],
+                        [],
+                        [],
+                        [],
+                        [$changedIndex],
+                        [],
+                        $schemaDiff->fromSchema->getTable($changedTable->name)
+                    );
+
+                    $temporarySchemaDiff = GeneralUtility::makeInstance(
+                        SchemaDiff::class,
+                        [],
+                        [$indexDiff],
+                        [],
+                        $schemaDiff->fromSchema
+                    );
+
+                    $statements = $temporarySchemaDiff->toSql($databasePlatform);
+                    foreach ($statements as $statement) {
+                        $updateSuggestions['change'][md5($statement)] = $statement;
+                    }
+                }
+            }
+
+            // Treat renamed indexes as a field change as it's a simple rename operation
+            if (count($changedTable->renamedIndexes) !== 0) {
+                // Create a base table diff without any changes, there's no constructor
+                // argument to pass in renamed columns.
+                $tableDiff = GeneralUtility::makeInstance(
+                    TableDiff::class,
+                    $changedTable->name,
+                    [],
+                    [],
+                    [],
+                    [],
+                    [],
+                    [],
+                    $schemaDiff->fromSchema->getTable($changedTable->name)
+                );
+
+                // Treat each renamed index with a new diff to get a dedicated suggestions
+                // just for this index.
+                foreach ($changedTable->renamedIndexes as $key => $renamedIndex) {
+                    $indexDiff = clone $tableDiff;
+                    $indexDiff->renamedIndexes = [$key => $renamedIndex];
+
+                    $temporarySchemaDiff = GeneralUtility::makeInstance(
+                        SchemaDiff::class,
+                        [],
+                        [$indexDiff],
+                        [],
+                        $schemaDiff->fromSchema
+                    );
+
+                    $statements = $temporarySchemaDiff->toSql($databasePlatform);
+                    foreach ($statements as $statement) {
+                        $updateSuggestions['change'][md5($statement)] = $statement;
+                    }
+                }
+            }
+
+            // Treat each changed foreign key with a new diff to get a dedicated suggestions
+            // just for this foreign key.
+            if (count($changedTable->changedForeignKeys) !== 0) {
+                $tableDiff = GeneralUtility::makeInstance(
+                    TableDiff::class,
+                    $changedTable->name,
+                    [],
+                    [],
+                    [],
+                    [],
+                    [],
+                    [],
+                    $schemaDiff->fromSchema->getTable($changedTable->name)
+                );
+
+                foreach ($changedTable->changedForeignKeys as $changedForeignKey) {
+                    $foreignKeyDiff = clone $tableDiff;
+                    $foreignKeyDiff->changedForeignKeys = [$changedForeignKey];
+
+                    $temporarySchemaDiff = GeneralUtility::makeInstance(
+                        SchemaDiff::class,
+                        [],
+                        [$foreignKeyDiff],
+                        [],
+                        $schemaDiff->fromSchema
+                    );
+
+                    $statements = $temporarySchemaDiff->toSql($databasePlatform);
+                    foreach ($statements as $statement) {
+                        $updateSuggestions['change'][md5($statement)] = $statement;
+                    }
+                }
+            }
+        }
+
+        return $updateSuggestions;
+    }
+
+    /**
+     * Extract update suggestions (SQL statements) for tables that are
+     * no longer present in the expected schema from the schema diff.
+     * In this case the update suggestions are renames of the tables
+     * with a prefix to mark them for deletion in a second sweep.
+     *
+     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
+     * @param \TYPO3\CMS\Core\Database\Connection $connection
+     * @return array
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \InvalidArgumentException
+     */
+    protected function getUnusedTableUpdateSuggestions(SchemaDiff $schemaDiff, Connection $connection): array
+    {
+        $updateSuggestions = [];
+        foreach ($schemaDiff->changedTables as $tableDiff) {
+            // Skip tables that are not being renamed or where the new name isn't prefixed
+            // with the deletion marker.
+            if ($tableDiff->getNewName() === false
+                || !StringUtility::beginsWith($tableDiff->getNewName()->getName(), $this->deletedPrefix)
+            ) {
+                continue;
+            }
+            // Build a new schema diff that only contains this table
+            $changedFieldDiff = GeneralUtility::makeInstance(
+                SchemaDiff::class,
+                [],
+                [$tableDiff],
+                [],
+                $schemaDiff->fromSchema
+            );
+
+            $statements = $changedFieldDiff->toSql($connection->getDatabasePlatform());
+
+            foreach ($statements as $statement) {
+                $updateSuggestions['change_table'][md5($statement)] = $statement;
+            }
+            $updateSuggestions['tables_count'][md5($statements[0])] = $this->getTableRecordCount($tableDiff->name);
+        }
+
+        return $updateSuggestions;
+    }
+
+    /**
+     * Extract update suggestions (SQL statements) for fields that are
+     * no longer present in the expected schema from the schema diff.
+     * In this case the update suggestions are renames of the fields
+     * with a prefix to mark them for deletion in a second sweep.
+     *
+     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
+     * @param \TYPO3\CMS\Core\Database\Connection $connection
+     * @return array
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \InvalidArgumentException
+     */
+    protected function getUnusedFieldUpdateSuggestions(SchemaDiff $schemaDiff, Connection $connection): array
+    {
+        $changedTables = [];
+
+        foreach ($schemaDiff->changedTables as $index => $changedTable) {
+            if (count($changedTable->changedColumns) === 0) {
+                continue;
+            }
+
+            // Treat each changed column with a new diff to get a dedicated suggestions
+            // just for this single column.
+            foreach ($changedTable->changedColumns as $changedColumn) {
+                // Field has not been renamed
+                if ($changedColumn->getOldColumnName()->getName() === $changedColumn->column->getName()) {
+                    continue;
+                }
+
+                $changedTables[$index . ':' . $changedColumn->column->getName()] = GeneralUtility::makeInstance(
+                    TableDiff::class,
+                    $changedTable->name,
+                    [],
+                    [$changedColumn],
+                    [],
+                    [],
+                    [],
+                    [],
+                    $schemaDiff->fromSchema->getTable($changedTable->name)
+                );
+            }
+        }
+
+        // Build a new schema diff that only contains unused fields
+        $changedFieldDiff = GeneralUtility::makeInstance(
+            SchemaDiff::class,
+            [],
+            $changedTables,
+            [],
+            $schemaDiff->fromSchema
+        );
+
+        $statements = $changedFieldDiff->toSql($connection->getDatabasePlatform());
+
+        return ['change' => $this->calculateUpdateSuggestionsHashes($statements)];
+    }
+
+    /**
+     * Extract update suggestions (SQL statements) for fields that can
+     * be removed from the complete schema diff.
+     * Fields that can be removed have been prefixed in a previous run
+     * of the schema migration.
+     *
+     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
+     * @param \TYPO3\CMS\Core\Database\Connection $connection
+     * @return array
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \InvalidArgumentException
+     */
+    protected function getDropFieldUpdateSuggestions(SchemaDiff $schemaDiff, Connection $connection): array
+    {
+        $changedTables = [];
+
+        foreach ($schemaDiff->changedTables as $index => $changedTable) {
+            if (count($changedTable->removedColumns) !== 0) {
+                // Treat each changed column with a new diff to get a dedicated suggestions
+                // just for this single column.
+                foreach ($changedTable->removedColumns as $removedColumn) {
+                    $changedTables[$index . ':tbl_' . $removedColumn->getName()] = GeneralUtility::makeInstance(
+                        TableDiff::class,
+                        $changedTable->name,
+                        [],
+                        [],
+                        [$removedColumn],
+                        [],
+                        [],
+                        [],
+                        $schemaDiff->fromSchema->getTable($changedTable->name)
+                    );
+                }
+            }
+
+            if (count($changedTable->removedIndexes) !== 0) {
+                // Treat each removed index with a new diff to get a dedicated suggestions
+                // just for this index.
+                foreach ($changedTable->removedIndexes as $removedIndex) {
+                    $changedTables[$index . ':idx_' . $removedIndex->getName()] = GeneralUtility::makeInstance(
+                        TableDiff::class,
+                        $changedTable->name,
+                        [],
+                        [],
+                        [],
+                        [],
+                        [],
+                        [$removedIndex],
+                        $schemaDiff->fromSchema->getTable($changedTable->name)
+                    );
+                }
+            }
+
+            if (count($changedTable->removedForeignKeys) !== 0) {
+                // Treat each removed foreign key with a new diff to get a dedicated suggestions
+                // just for this foreign key.
+                foreach ($changedTable->removedForeignKeys as $removedForeignKey) {
+                    $fkIndex = $index . ':fk_' . $removedForeignKey->getName();
+                    $changedTables[$fkIndex] = GeneralUtility::makeInstance(
+                        TableDiff::class,
+                        $changedTable->name,
+                        [],
+                        [],
+                        [],
+                        [],
+                        [],
+                        [],
+                        $schemaDiff->fromSchema->getTable($changedTable->name)
+                    );
+                    $changedTables[$fkIndex]->removedForeignKeys = [$removedForeignKey];
+                }
+            }
+        }
+
+        // Build a new schema diff that only contains removable fields
+        $removedFieldDiff = GeneralUtility::makeInstance(
+            SchemaDiff::class,
+            [],
+            $changedTables,
+            [],
+            $schemaDiff->fromSchema
+        );
+
+        $statements = $removedFieldDiff->toSql($connection->getDatabasePlatform());
+
+        return ['drop' => $this->calculateUpdateSuggestionsHashes($statements)];
+    }
+
+    /**
+     * Extract update suggestions (SQL statements) for tables that can
+     * be removed from the complete schema diff.
+     * Tables that can be removed have been prefixed in a previous run
+     * of the schema migration.
+     *
+     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
+     * @param \TYPO3\CMS\Core\Database\Connection $connection
+     * @return array
+     * @throws \Doctrine\DBAL\Schema\SchemaException
+     * @throws \InvalidArgumentException
+     */
+    protected function getDropTableUpdateSuggestions(SchemaDiff $schemaDiff, Connection $connection): array
+    {
+        $updateSuggestions = [];
+        foreach ($schemaDiff->removedTables as $removedTable) {
+            // Build a new schema diff that only contains this table
+            $tableDiff = GeneralUtility::makeInstance(
+                SchemaDiff::class,
+                [],
+                [],
+                [$removedTable],
+                $schemaDiff->fromSchema
+            );
+
+            $statements = $tableDiff->toSql($connection->getDatabasePlatform());
+            foreach ($statements as $statement) {
+                $updateSuggestions['drop_table'][md5($statement)] = $statement;
+            }
+
+            // Only store the record count for this table for the first statement,
+            // assuming that this is the actual DROP TABLE statement.
+            $updateSuggestions['tables_count'][md5($statements[0])] = $this->getTableRecordCount(
+                $removedTable->getName()
+            );
+        }
+
+        return $updateSuggestions;
+    }
+
+    /**
+     * Move tables to be removed that are not prefixed with the deleted prefix to the list
+     * of changed tables and set a new prefixed name.
+     * Without this help the Doctrine SchemaDiff has no idea if a table has been renamed and
+     * performs a drop of the old table and creates a new table, which leads to all data in
+     * the old table being lost.
+     *
+     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
+     * @return \Doctrine\DBAL\Schema\SchemaDiff
+     * @throws \InvalidArgumentException
+     */
+    protected function migrateUnprefixedRemovedTablesToRenames(SchemaDiff $schemaDiff): SchemaDiff
+    {
+        foreach ($schemaDiff->removedTables as $index => $removedTable) {
+            if (StringUtility::beginsWith($removedTable->getName(), $this->deletedPrefix)) {
+                continue;
+            }
+            $tableDiff = GeneralUtility::makeInstance(
+                TableDiff::class,
+                $removedTable->getName(),
+                $addedColumns = [],
+                $changedColumns = [],
+                $removedColumns = [],
+                $addedIndexes = [],
+                $changedIndexes = [],
+                $removedIndexes = [],
+                $fromTable = $removedTable
+            );
+
+            $tableDiff->newName = $this->deletedPrefix . $removedTable->getName();
+            $schemaDiff->changedTables[$index] = $tableDiff;
+            unset($schemaDiff->removedTables[$index]);
+        }
+
+        return $schemaDiff;
+    }
+
+    /**
+     * Scan the list of changed tables for fields that are going to be dropped. If
+     * the name of the field does not start with the deleted prefix mark the column
+     * for a rename instead of a drop operation.
+     *
+     * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff
+     * @return \Doctrine\DBAL\Schema\SchemaDiff
+     * @throws \InvalidArgumentException
+     */
+    protected function migrateUnprefixedRemovedFieldsToRenames(SchemaDiff $schemaDiff): SchemaDiff
+    {
+        foreach ($schemaDiff->changedTables as $tableIndex => $changedTable) {
+            if (count($changedTable->removedColumns) === 0) {
+                continue;
+            }
+
+            foreach ($changedTable->removedColumns as $columnIndex => $removedColumn) {
+                if (StringUtility::beginsWith($removedColumn->getName(), $this->deletedPrefix)) {
+                    continue;
+                }
+
+                // Build a new column object with the same properties as the removed column
+                $renamedColumn = new Column(
+                    $this->deletedPrefix . $removedColumn->getName(),
+                    $removedColumn->getType(),
+                    array_diff_key($removedColumn->toArray(), ['name', 'type'])
+                );
+
+                // Build the diff object for the column to rename
+                $columnDiff = GeneralUtility::makeInstance(
+                    ColumnDiff::class,
+                    $removedColumn->getName(),
+                    $renamedColumn,
+                    $changedProperties = [],
+                    $removedColumn
+                );
+
+                // Add the column with the required rename information to the changed column list
+                $schemaDiff->changedTables[$tableIndex]->changedColumns[$columnIndex] = $columnDiff;
+
+                // Remove the column from the list of columns to be dropped
+                unset($schemaDiff->changedTables[$tableIndex]->removedColumns[$columnIndex]);
+            }
+        }
+
+        return $schemaDiff;
+    }
+
+    /**
+     * Return the amount of records in the given table.
+     *
+     * @param string $tableName
+     * @return int
+     * @throws \InvalidArgumentException
+     */
+    protected function getTableRecordCount(string $tableName): int
+    {
+        return GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getConnectionForTable($tableName)
+            ->count('*', $tableName, []);
+    }
+
+    /**
+     * Determine the connection name for a table
+     *
+     * @param string $tableName
+     * @return string
+     * @throws \InvalidArgumentException
+     */
+    protected function getConnectionNameForTable(string $tableName): string
+    {
+        $connectionNames = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionNames();
+
+        if (array_key_exists($tableName, (array)$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])) {
+            return in_array($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName], $connectionNames, true)
+                ? $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName]
+                : ConnectionPool::DEFAULT_CONNECTION_NAME;
+        }
+
+        return ConnectionPool::DEFAULT_CONNECTION_NAME;
+    }
+
+    /**
+     * Replace the array keys with a md5 sum of the actual SQL statement
+     *
+     * @param string[] $statements
+     * @return string[]
+     */
+    protected function calculateUpdateSuggestionsHashes(array $statements): array
+    {
+        return array_combine(array_map('md5', $statements), $statements);
+    }
+
+    /**
+     * Helper for buildSchemaDiff to filter an array of TableDiffs against a list of valid table names.
+     *
+     * @param TableDiff[]|Table[] $tableDiffs
+     * @param string[] $validTableNames
+     * @return TableDiff[]
+     * @throws \InvalidArgumentException
+     */
+    protected function removeUnrelatedTables(array $tableDiffs, array $validTableNames): array
+    {
+        return array_filter(
+            $tableDiffs,
+            function ($table) use ($validTableNames) {
+                if ($table instanceof Table) {
+                    $tableName = $table->getName();
+                } else {
+                    $tableName = $table->newName ?: $table->name;
+                }
+
+                // If the tablename has a deleted prefix strip it of before comparing
+                // it against the list of valid table names so that drop operations
+                // don't get removed.
+                if (StringUtility::beginsWith($tableName, $this->deletedPrefix)) {
+                    $tableName = substr($tableName, strlen($this->deletedPrefix));
+                }
+                return in_array($tableName, $validTableNames, true)
+                    || in_array($this->deletedPrefix . $tableName, $validTableNames, true);
+            }
+        );
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/SqlReader.php b/typo3/sysext/core/Classes/Database/Schema/SqlReader.php
new file mode 100644 (file)
index 0000000..095b45c
--- /dev/null
@@ -0,0 +1,169 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Core\Database\Schema;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
+use TYPO3\CMS\Install\Service\SqlExpectedSchemaService;
+
+/**
+ * Helper methods to handle raw SQL input and transform it into individual statements
+ * for further processing.
+ *
+ * @internal
+ */
+class SqlReader
+{
+    /**
+     * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
+     */
+    protected $signalSlotDispatcher;
+
+    /**
+     * @param Dispatcher $signalSlotDispatcher
+     * @throws \InvalidArgumentException
+     */
+    public function __construct(Dispatcher $signalSlotDispatcher = null)
+    {
+        $this->signalSlotDispatcher = $signalSlotDispatcher ?: GeneralUtility::makeInstance(Dispatcher::class);
+    }
+
+    /**
+     * Cycle through all loaded extensions and get full table definitions as concatenated string
+     *
+     * @param bool $withStatic TRUE if sql from ext_tables_static+adt.sql should be loaded, too.
+     * @return string Concatenated SQL of loaded extensions ext_tables.sql
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
+     */
+    public function getTablesDefinitionString(bool $withStatic = false): string
+    {
+        $sqlString = [];
+
+        // Find all ext_tables.sql of loaded extensions
+        foreach ((array)$GLOBALS['TYPO3_LOADED_EXT'] as $extensionConfiguration) {
+            if (!is_array($extensionConfiguration) && !$extensionConfiguration instanceof \ArrayAccess) {
+                continue;
+            }
+            if ($extensionConfiguration['ext_tables.sql']) {
+                $sqlString[] = file_get_contents($extensionConfiguration['ext_tables.sql']);
+            }
+            if ($withStatic && $extensionConfiguration['ext_tables_static+adt.sql']) {
+                $sqlString[] = file_get_contents($extensionConfiguration['ext_tables_static+adt.sql']);
+            }
+        }
+
+        $sqlString = $this->emitTablesDefinitionIsBeingBuiltSignal($sqlString);
+
+        return implode(LF . LF, $sqlString);
+    }
+
+    /**
+     * Returns an array where every entry is a single SQL-statement.
+     * Input must be formatted like an ordinary MySQL dump file. Every statements needs to be terminated by a ';'
+     * and there may only be one statement (or partial statement) per line.
+     *
+     * @param string $dumpContent The SQL dump content.
+     * @param string $queryRegex Regex to select which statements to return.
+     * @return array Array of SQL statements
+     */
+    public function getStatementArray(string $dumpContent, string $queryRegex = null): array
+    {
+        $statementArray = [];
+        $statementArrayPointer = 0;
+        foreach (explode(LF, $dumpContent) as $lineContent) {
+            $lineContent = trim($lineContent);
+
+            // Skip empty lines and comments
+            if ($lineContent === '' || $lineContent[0] === '#' || strpos($lineContent, '--') === 0) {
+                continue;
+            }
+
+            $statementArray[$statementArrayPointer] .= $lineContent;
+
+            if (substr($lineContent, -1) === ';') {
+                $statement = trim($statementArray[$statementArrayPointer]);
+                if (!$statement || ($queryRegex && !preg_match('/' . $queryRegex . '/i', $statement))) {
+                    unset($statementArray[$statementArrayPointer]);
+                }
+                $statementArrayPointer++;
+            } else {
+                $statementArray[$statementArrayPointer] .= LF;
+            }
+        }
+
+        return $statementArray;
+    }
+
+    /**
+     * Extract only INSERT statements from SQL dump
+     *
+     * @param string $dumpContent
+     * @return array
+     */
+    public function getInsertStatementArray(string $dumpContent): array
+    {
+        return $this->getStatementArray($dumpContent, '^INSERT');
+    }
+
+    /**
+     * Extract only CREATE TABLE statements from SQL dump
+     *
+     * @param string $dumpContent
+     * @return array
+     */
+    public function getCreateTableStatementArray(string $dumpContent): array
+    {
+        return $this->getStatementArray($dumpContent, '^CREATE TABLE');
+    }
+
+    /**
+     * Emits a signal to manipulate the tables definitions
+     *
+     * @param array $sqlString
+     * @return array
+     * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
+     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
+     */
+    protected function emitTablesDefinitionIsBeingBuiltSignal(array $sqlString): array
+    {
+        // Using the old class name from the install tool here to keep backwards compatibility.
+        $signalReturn = $this->signalSlotDispatcher->dispatch(
+            SqlExpectedSchemaService::class,
+            'tablesDefinitionIsBeingBuilt',
+            [$sqlString]
+        );
+
+        // This is important to support old associated returns
+        $signalReturn = array_values($signalReturn);
+        $sqlString = $signalReturn[0];
+        if (!is_array($sqlString)) {
+            throw new Exception\UnexpectedSignalReturnValueTypeException(
+                sprintf(
+                    'The signal %s of class %s returned a value of type %s, but array was expected.',
+                    'tablesDefinitionIsBeingBuilt',
+                    __CLASS__,
+                    gettype($sqlString)
+                ),
+                1382351456
+            );
+        }
+
+        return $sqlString;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Types/EnumType.php b/typo3/sysext/core/Classes/Database/Schema/Types/EnumType.php
new file mode 100644 (file)
index 0000000..1f42cdd
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Core\Database\Schema\Types;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+use Doctrine\DBAL\Types\Type;
+
+/**
+ * Type that maps an TYPE field.
+ */
+class EnumType extends Type
+{
+    const TYPE = 'enum';
+
+    /**
+     * Gets the SQL declaration snippet for a field of this type.
+     *
+     * @param array $fieldDeclaration The field declaration.
+     * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform The currently used database platform.
+     *
+     * @return string
+     */
+    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
+    {
+        $quotedValues = array_map([$platform, 'quoteStringLiteral'], $fieldDeclaration['unquotedValues']);
+
+        return sprintf('ENUM(%s)', implode(', ', $quotedValues));
+    }
+
+    /**
+     * Gets the name of this type.
+     *
+     * @return string
+     */
+    public function getName(): string
+    {
+        return static::TYPE;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Schema/Types/SetType.php b/typo3/sysext/core/Classes/Database/Schema/Types/SetType.php
new file mode 100644 (file)
index 0000000..1688d3a
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Core\Database\Schema\Types;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+use Doctrine\DBAL\Types\Type;
+
+/**
+ * Type that maps an TYPE field.
+ */
+class SetType extends Type
+{
+    const TYPE = 'set';
+
+    /**
+     * Gets the SQL declaration snippet for a field of this type.
+     *
+     * @param array $fieldDeclaration The field declaration.
+     * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform The currently used database platform.
+     *
+     * @return string
+     */
+    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
+    {
+        $quotedValues = array_map([$platform, 'quoteStringLiteral'], $fieldDeclaration['unquotedValues']);
+
+        return sprintf('SET(%s)', implode(', ', $quotedValues));
+    }
+
+    /**
+     * Gets the name of this type.
+     *
+     * @return string
+     */
+    public function getName(): string
+    {
+        return static::TYPE;
+    }
+}
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-77643-ReimplementSqlSchemaMigrationServiceUsingDoctrineSchemaManager.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-77643-ReimplementSqlSchemaMigrationServiceUsingDoctrineSchemaManager.rst
new file mode 100644 (file)
index 0000000..06380ad
--- /dev/null
@@ -0,0 +1,22 @@
+====================================================================================
+Feature: #77643 - Reimplement SqlSchemaMigrationService using Doctrine SchemaManager
+====================================================================================
+
+Description
+===========
+
+The SqlSchemaMigrationService has been reimplemented using a LL(*) Parser for CREATE TABLE
+statements. The new parser supports MySQL syntax for CREATE TABLE statements. Based on the
+abstract syntax tree produced by this parser Doctrine Table objects are created that
+implement a DBMS independent representation of the schema and are used with the Doctrine
+SchemaManager to handle the schema migrations needs of the TYPO3 core.
+
+
+Impact
+======
+
+Update suggestions from the new SchemaMigrator are per connection, on all additional
+connections only explicitly mapped tables are managed. MySQL specific data types are being
+mapped to the closest matching standard type, for example TINYINT to SMALLINT. The support
+for foreign keys has been enhanced as a result of the additional capabilities of the
+Doctrine SchemaManager.
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/addColumnsToTable.sql b/typo3/sysext/core/Tests/Functional/Database/Fixtures/addColumnsToTable.sql
new file mode 100644 (file)
index 0000000..dc341ed
--- /dev/null
@@ -0,0 +1,4 @@
+CREATE TABLE aTestTable (
+       title VARCHAR(50) DEFAULT ''          NOT NULL,
+       description TEXT
+);
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/addCreateChange.sql b/typo3/sysext/core/Tests/Functional/Database/Fixtures/addCreateChange.sql
new file mode 100644 (file)
index 0000000..2e2dc59
--- /dev/null
@@ -0,0 +1,10 @@
+CREATE TABLE aTestTable (
+       pid   BIGINT(11) UNSIGNED             NOT NULL,
+       title VARCHAR(50) DEFAULT ''          NOT NULL,
+       UNIQUE title (title)
+);
+
+CREATE TABLE anotherTestTable (
+       uid   INT(11) UNSIGNED                NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       title VARCHAR(50) DEFAULT ''          NOT NULL
+);
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/changeExistingColumn.sql b/typo3/sysext/core/Tests/Functional/Database/Fixtures/changeExistingColumn.sql
new file mode 100644 (file)
index 0000000..cac9032
--- /dev/null
@@ -0,0 +1,3 @@
+CREATE TABLE aTestTable (
+       uid BIGINT(11) NOT NULL AUTO_INCREMENT,
+);
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/defaultNullWithoutNotNull.sql b/typo3/sysext/core/Tests/Functional/Database/Fixtures/defaultNullWithoutNotNull.sql
new file mode 100644 (file)
index 0000000..c20c0d7
--- /dev/null
@@ -0,0 +1,3 @@
+CREATE TABLE aTestTable (
+       aTestField DECIMAL (5, 2) UNSIGNED NULL DEFAULT NULL
+);
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/ifNotExists.sql b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ifNotExists.sql
new file mode 100644 (file)
index 0000000..ba7d282
--- /dev/null
@@ -0,0 +1,4 @@
+CREATE TABLE IF NOT EXISTS `anotherTestTable` (
+       `uid`     INT(11) UNSIGNED                NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       `pid`     INT(11) UNSIGNED DEFAULT '0'    NOT NULL
+);
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/importStaticData.sql b/typo3/sysext/core/Tests/Functional/Database/Fixtures/importStaticData.sql
new file mode 100644 (file)
index 0000000..4e1b80b
--- /dev/null
@@ -0,0 +1,8 @@
+-- CREATE TABLE in static data import is ignored
+CREATE TABLE anotherTestTable (
+       uid   INT(11) UNSIGNED                NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       title VARCHAR(50) DEFAULT ''          NOT NULL
+);
+
+INSERT INTO aTestTable VALUES (NULL, 0, 0, 0, 0);
+INSERT INTO `aTestTable` VALUES (NULL, 1, 1, 1, 1);
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/newTable.sql b/typo3/sysext/core/Tests/Functional/Database/Fixtures/newTable.sql
new file mode 100644 (file)
index 0000000..2dfb653
--- /dev/null
@@ -0,0 +1,10 @@
+CREATE TABLE aTestTable (
+       uid     INT(11) UNSIGNED                NOT NULL AUTO_INCREMENT,
+       pid     INT(11) UNSIGNED DEFAULT '0'    NOT NULL,
+       tstamp  INT(11) UNSIGNED DEFAULT '0'    NOT NULL,
+       hidden  TINYINT(3) UNSIGNED DEFAULT '0' NOT NULL,
+       deleted TINYINT(3) UNSIGNED DEFAULT '0' NOT NULL,
+
+       PRIMARY KEY (uid),
+       KEY parent (pid)
+);
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/notNullWithoutDefaultValue.sql b/typo3/sysext/core/Tests/Functional/Database/Fixtures/notNullWithoutDefaultValue.sql
new file mode 100644 (file)
index 0000000..f76eb25
--- /dev/null
@@ -0,0 +1,3 @@
+CREATE TABLE aTestTable (
+       aTestField INT(11) NOT NULL
+);
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/unusedColumn.sql b/typo3/sysext/core/Tests/Functional/Database/Fixtures/unusedColumn.sql
new file mode 100644 (file)
index 0000000..6f6cf64
--- /dev/null
@@ -0,0 +1,9 @@
+CREATE TABLE aTestTable (
+       uid     INT(11) UNSIGNED                NOT NULL AUTO_INCREMENT,
+       pid     INT(11) UNSIGNED DEFAULT '0'    NOT NULL,
+       tstamp  INT(11) UNSIGNED DEFAULT '0'    NOT NULL,
+       deleted TINYINT(3) UNSIGNED DEFAULT '0' NOT NULL,
+
+       PRIMARY KEY (uid),
+       KEY parent (pid)
+);
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/unusedTable.sql b/typo3/sysext/core/Tests/Functional/Database/Fixtures/unusedTable.sql
new file mode 100644 (file)
index 0000000..969eeb3
--- /dev/null
@@ -0,0 +1,10 @@
+CREATE TABLE anotherTestTable (
+       uid     INT(11) UNSIGNED                NOT NULL AUTO_INCREMENT,
+       pid     INT(11) UNSIGNED DEFAULT '0'    NOT NULL,
+       tstamp  INT(11) UNSIGNED DEFAULT '0'    NOT NULL,
+       hidden  TINYINT(3) UNSIGNED DEFAULT '0' NOT NULL,
+       deleted TINYINT(3) UNSIGNED DEFAULT '0' NOT NULL,
+
+       PRIMARY KEY (uid),
+       KEY parent (pid)
+);
diff --git a/typo3/sysext/core/Tests/Functional/Database/Schema/SchemaMigrationServiceTest.php b/typo3/sysext/core/Tests/Functional/Database/Schema/SchemaMigrationServiceTest.php
new file mode 100644 (file)
index 0000000..01564b5
--- /dev/null
@@ -0,0 +1,369 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Functional\Category\Collection;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Doctrine\DBAL\Schema\AbstractSchemaManager;
+use Doctrine\DBAL\Schema\Table;
+use Doctrine\DBAL\Types\BigIntType;
+use Doctrine\DBAL\Types\IntegerType;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
+use TYPO3\CMS\Core\Database\Schema\SqlReader;
+use TYPO3\CMS\Core\Tests\FunctionalTestCase;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Test case for \TYPO3\CMS\Core\Database\Schema\SchemaMigrationServiceTest
+ */
+class SchemaMigrationServiceTest extends FunctionalTestCase
+{
+    /**
+     * @var SqlReader
+     */
+    protected $sqlReader;
+
+    /**
+     * @var ConnectionPool
+     */
+    protected $connectionPool;
+
+    /**
+     * @var AbstractSchemaManager
+     */
+    protected $schemaManager;
+
+    /**
+     * @var \TYPO3\CMS\Core\Database\Schema\SchemaMigrator
+     */
+    protected $subject;
+
+    /**
+     * @var string
+     */
+    protected $tableName = 'aTestTable';
+
+    /**
+     * Sets up this test suite.
+     *
+     * @return void
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->subject = GeneralUtility::makeInstance(SchemaMigrator::class);
+        $this->sqlReader = GeneralUtility::makeInstance(SqlReader::class);
+        $this->connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $this->schemaManager = $this->connectionPool->getConnectionForTable($this->tableName)->getSchemaManager();
+        $this->prepareTestTable();
+    }
+
+    /**
+     * Tears down this test suite.
+     */
+    protected function tearDown()
+    {
+        parent::tearDown();
+
+        if ($this->schemaManager->tablesExist([$this->tableName])) {
+            $this->schemaManager->dropTable($this->tableName);
+        }
+        if ($this->schemaManager->tablesExist(['zzz_deleted_' . $this->tableName])) {
+            $this->schemaManager->dropTable('zzz_deleted_' . $this->tableName);
+        }
+        if ($this->schemaManager->tablesExist(['anotherTestTable'])) {
+            $this->schemaManager->dropTable('anotherTestTable');
+        }
+    }
+
+    /**
+     * @test
+     */
+    public function createNewTable()
+    {
+        if ($this->schemaManager->tablesExist([$this->tableName])) {
+            $this->schemaManager->dropTable($this->tableName);
+        }
+
+        $statements = $this->readFixtureFile('newTable');
+        $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
+
+        $this->subject->migrate(
+            $statements,
+            $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['create_table']
+        );
+
+        $this->assertCount(5, $this->getTableDetails()->getColumns());
+    }
+
+    /**
+     * @test
+     */
+    public function createNewTableIfNotExists()
+    {
+        $statements = $this->readFixtureFile('ifNotExists');
+        $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
+
+        $this->subject->migrate(
+            $statements,
+            $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['create_table']
+        );
+
+        $this->assertTrue($this->schemaManager->tablesExist(['anotherTestTable']));
+    }
+
+    /**
+     * @test
+     */
+    public function addNewColumns()
+    {
+        $statements = $this->readFixtureFile('addColumnsToTable');
+        $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
+
+        $this->subject->migrate(
+            $statements,
+            $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['add']
+        );
+
+        $this->assertCount(7, $this->getTableDetails()->getColumns());
+        $this->assertTrue($this->getTableDetails()->hasColumn('title'));
+        $this->assertTrue($this->getTableDetails()->hasColumn('description'));
+    }
+
+    /**
+     * @test
+     */
+    public function changeExistingColumn()
+    {
+        $statements = $this->readFixtureFile('changeExistingColumn');
+        $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
+
+        $this->assertInstanceOf(IntegerType::class, $this->getTableDetails()->getColumn('uid')->getType());
+        $this->assertTrue($this->getTableDetails()->getColumn('uid')->getUnsigned());
+
+        $this->subject->migrate(
+            $statements,
+            $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['change']
+        );
+
+        $this->assertInstanceOf(BigIntType::class, $this->getTableDetails()->getColumn('uid')->getType());
+        $this->assertFalse($this->getTableDetails()->getColumn('uid')->getUnsigned());
+    }
+
+    /**
+     * @test
+     */
+    public function notNullWithoutDefaultValue()
+    {
+        $statements = $this->readFixtureFile('notNullWithoutDefaultValue');
+        $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
+
+        $this->subject->migrate(
+            $statements,
+            $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['add']
+        );
+
+        $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
+        $this->assertEmpty($updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['change']);
+        $this->assertTrue($this->getTableDetails()->getColumn('aTestField')->getNotnull());
+    }
+
+    /**
+     * @test
+     */
+    public function defaultNullWithoutNotNull()
+    {
+        $statements = $this->readFixtureFile('defaultNullWithoutNotNull');
+        $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
+
+        $this->subject->migrate(
+            $statements,
+            $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['add']
+        );
+
+        $updateSuggestions = $this->subject->getUpdateSuggestions($statements);
+        $this->assertEmpty($updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['change']);
+        $this->assertFalse($this->getTableDetails()->getColumn('aTestField')->getNotnull());
+        $this->assertNull($this->getTableDetails()->getColumn('aTestField')->getDefault());
+    }
+
+    /**
+     * @test
+     */
+    public function renameUnusedField()
+    {
+        $statements = $this->readFixtureFile('unusedColumn');
+        $updateSuggestions = $this->subject->getUpdateSuggestions($statements, true);
+
+        $this->subject->migrate(
+            $statements,
+            $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['change']
+        );
+
+        $this->assertFalse($this->getTableDetails()->hasColumn('hidden'));
+        $this->assertTrue($this->getTableDetails()->hasColumn('zzz_deleted_hidden'));
+    }
+
+    /**
+     * @test
+     */
+    public function renameUnusedTable()
+    {
+        $statements = $this->readFixtureFile('unusedTable');
+        $updateSuggestions = $this->subject->getUpdateSuggestions($statements, true);
+
+        $this->subject->migrate(
+            $statements,
+            $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['change_table']
+        );
+
+        $this->assertNotContains($this->tableName, $this->schemaManager->listTableNames());
+        $this->assertContains('zzz_deleted_' . $this->tableName, $this->schemaManager->listTableNames());
+    }
+
+    /**
+     * @test
+     */
+    public function dropUnusedField()
+    {
+        $connection = $this->connectionPool->getConnectionForTable($this->tableName);
+        $fromSchema = $this->schemaManager->createSchema();
+        $toSchema = clone $fromSchema;
+        $toSchema->getTable($this->tableName)->addColumn('zzz_deleted_testfield', 'integer');
+        $statements = $fromSchema->getMigrateToSql(
+            $toSchema,
+            $connection->getDatabasePlatform()
+        );
+        $connection->executeUpdate($statements[0]);
+        $this->assertTrue($this->getTableDetails()->hasColumn('zzz_deleted_testfield'));
+
+        $statements = $this->readFixtureFile('newTable');
+        $updateSuggestions = $this->subject->getUpdateSuggestions($statements, true);
+        $this->subject->migrate(
+            $statements,
+            $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['drop']
+        );
+
+        $this->assertFalse($this->getTableDetails()->hasColumn('zzz_deleted_testfield'));
+    }
+
+    /**
+     * @test
+     */
+    public function dropUnusedTable()
+    {
+        $this->schemaManager->renameTable($this->tableName, 'zzz_deleted_' . $this->tableName);
+        $this->assertNotContains($this->tableName, $this->schemaManager->listTableNames());
+        $this->assertContains('zzz_deleted_' . $this->tableName, $this->schemaManager->listTableNames());
+
+        $statements = $this->readFixtureFile('newTable');
+        $updateSuggestions = $this->subject->getUpdateSuggestions($statements, true);
+        $this->subject->migrate(
+            $statements,
+            $updateSuggestions[ConnectionPool::DEFAULT_CONNECTION_NAME]['drop_table']
+        );
+
+        $this->assertNotContains($this->tableName, $this->schemaManager->listTableNames());
+        $this->assertNotContains('zzz_deleted_' . $this->tableName, $this->schemaManager->listTableNames());
+    }
+
+    /**
+     * @test
+     */
+    public function installPerformsOnlyAddAndCreateOperations()
+    {
+        $statements = $this->readFixtureFile('addCreateChange');
+        $this->subject->install($statements, true);
+
+        $this->assertContains('anotherTestTable', $this->schemaManager->listTableNames());
+        $this->assertTrue($this->getTableDetails()->hasColumn('title'));
+        $this->assertTrue($this->getTableDetails()->hasIndex('title'));
+        $this->assertTrue($this->getTableDetails()->getIndex('title')->isUnique());
+        $this->assertNotInstanceOf(BigIntType::class, $this->getTableDetails()->getColumn('pid')->getType());
+    }
+
+    /**
+     * @test
+     */
+    public function installCanPerformChangeOperations()
+    {
+        $statements = $this->readFixtureFile('addCreateChange');
+        $this->subject->install($statements);
+
+        $this->assertContains('anotherTestTable', $this->schemaManager->listTableNames());
+        $this->assertTrue($this->getTableDetails()->hasColumn('title'));
+        $this->assertTrue($this->getTableDetails()->hasIndex('title'));
+        $this->assertTrue($this->getTableDetails()->getIndex('title')->isUnique());
+        $this->assertInstanceOf(BigIntType::class, $this->getTableDetails()->getColumn('pid')->getType());
+    }
+
+    /**
+     * @test
+     */
+    public function importStaticDataInsertsRecords()
+    {
+        $sqlCode = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Fixtures', 'importStaticData.sql']));
+        $connection = $this->connectionPool->getConnectionForTable($this->tableName);
+        $statements = $this->sqlReader->getInsertStatementArray($sqlCode);
+        $this->subject->importStaticData($statements);
+
+        $this->assertEquals(2, $connection->count('*', $this->tableName, []));
+    }
+
+    /**
+     * @test
+     */
+    public function importStaticDataIgnoresTableDefinitions()
+    {
+        $sqlCode = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Fixtures', 'importStaticData.sql']));
+        $statements = $this->sqlReader->getStatementArray($sqlCode);
+        $this->subject->importStaticData($statements);
+
+        $this->assertNotContains('anotherTestTable', $this->schemaManager->listTableNames());
+    }
+
+    /**
+     * Create the base table for all migration tests
+     */
+    protected function prepareTestTable()
+    {
+        $statements = $this->readFixtureFile('newTable');
+        $this->subject->install($statements, true);
+    }
+
+    /**
+     * Helper to return the Doctrine Table object for the test table
+     *
+     * @return \Doctrine\DBAL\Schema\Table
+     */
+    protected function getTableDetails(): Table
+    {
+        return $this->schemaManager->listTableDetails($this->tableName);
+    }
+
+    /**
+     * Helper to read a fixture SQL file and convert it into a statement array.
+     *
+     * @param string $fixtureName
+     * @return array
+     */
+    protected function readFixtureFile(string $fixtureName): array
+    {
+        $sqlCode = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Fixtures', $fixtureName]) . '.sql');
+
+        return $this->sqlReader->getCreateTableStatementArray($sqlCode);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/EventListener/SchemaColumnDefinitionListenerTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/EventListener/SchemaColumnDefinitionListenerTest.php
new file mode 100644 (file)
index 0000000..dbc1933
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Doctrine\DBAL\Event\SchemaColumnDefinitionEventArgs;
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+use Doctrine\DBAL\Schema\Column;
+use Doctrine\DBAL\Types\Type;
+use Prophecy\Prophecy\ObjectProphecy;
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\Schema\SchemaColumnDefinitionListener;
+use TYPO3\CMS\Core\Database\Schema\Types\EnumType;
+use TYPO3\CMS\Core\Database\Schema\Types\SetType;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Test case
+ */
+class SchemaColumnDefinitionListenerTest extends UnitTestCase
+{
+    /**
+     * @var SchemaColumnDefinitionListener
+     */
+    protected $subject;
+
+    /**
+     * @var Connection|ObjectProphecy
+     */
+    protected $connectionProphet;
+
+    /**
+     * Set up the test subject
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->subject = GeneralUtility::makeInstance(SchemaColumnDefinitionListener::class);
+        $this->connectionProphet = $this->prophesize(Connection::class);
+    }
+
+    /**
+     * @test
+     */
+    public function isInactiveForStandardColumnTypes()
+    {
+        $event = new SchemaColumnDefinitionEventArgs(
+            ['Type' => 'int(11)'],
+            'aTestTable',
+            'aTestDatabase',
+            $this->connectionProphet->reveal()
+        );
+
+        $this->subject->onSchemaColumnDefinition($event);
+        $this->assertNotTrue($event->isDefaultPrevented());
+        $this->assertNull($event->getColumn());
+    }
+
+    /**
+     * @test
+     */
+    public function buildsColumnForEnumDataType()
+    {
+        Type::addType('enum', EnumType::class);
+        $databasePlatformProphet = $this->prophesize(AbstractPlatform::class);
+        $databasePlatformProphet->getDoctrineTypeMapping('enum')->willReturn('enum');
+        $this->connectionProphet->getDatabasePlatform()->willReturn($databasePlatformProphet->reveal());
+
+        $event = new SchemaColumnDefinitionEventArgs(
+            ['Type' => "enum('value1', 'value2','value3')"],
+            'aTestTable',
+            'aTestDatabase',
+            $this->connectionProphet->reveal()
+        );
+
+        $this->subject->onSchemaColumnDefinition($event);
+        $this->assertTrue($event->isDefaultPrevented());
+        $this->assertInstanceOf(Column::class, $event->getColumn());
+        $this->assertInstanceOf(EnumType::class, $event->getColumn()->getType());
+        $this->assertSame(['value1', 'value2', 'value3'], $event->getColumn()->getPlatformOption('unquotedValues'));
+    }
+
+    /**
+     * @test
+     */
+    public function buildsColumnForSetDataType()
+    {
+        Type::addType('set', SetType::class);
+        $databasePlatformProphet = $this->prophesize(AbstractPlatform::class);
+        $databasePlatformProphet->getDoctrineTypeMapping('set')->willReturn('set');
+        $this->connectionProphet->getDatabasePlatform()->willReturn($databasePlatformProphet->reveal());
+
+        $event = new SchemaColumnDefinitionEventArgs(
+            ['Type' => "set('value1', 'value3')"],
+            'aTestTable',
+            'aTestDatabase',
+            $this->connectionProphet->reveal()
+        );
+
+        $this->subject->onSchemaColumnDefinition($event);
+        $this->assertTrue($event->isDefaultPrevented());
+        $this->assertInstanceOf(Column::class, $event->getColumn());
+        $this->assertInstanceOf(SetType::class, $event->getColumn()->getType());
+        $this->assertSame(['value1', 'value3'], $event->getColumn()->getPlatformOption('unquotedValues'));
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Fixtures/tablebuilder.sql b/typo3/sysext/core/Tests/Unit/Database/Schema/Fixtures/tablebuilder.sql
new file mode 100644 (file)
index 0000000..e873a4a
--- /dev/null
@@ -0,0 +1,20 @@
+#
+# Table structure for TableBuilder test
+# This table is formatted in different styles by intention!
+#
+CREATE TABLE aTestTable (
+       -- AUTO_INCREMENT + DEFAULT '0' is invalid, combination is here to check
+       -- that the tablebuilder ignores the default value in this combination.
+  uid INT(11) DEFAULT '0' NOT NULL AUTO_INCREMENT,
+  pid INT(11) DEFAULT '0' NOT NULL,
+       tstamp int(11) unsigned DEFAULT '0' NOT NULL,
+       sorting int(11) unsigned DEFAULT 0 NOT NULL,
+       deleted tinyint(1) unsigned DEFAULT '0' NOT NULL,
+       title varchar(255) DEFAULT '' NOT NULL,
+       `TSconfig` text,
+       no_cache int(10) unsigned DEFAULT '0' NOT NULL,
+       PRIMARY KEY (uid),
+  UNIQUE `parent` (pid,`deleted`,sorting),
+       KEY noCache (`no_cache`),
+       FOREIGN KEY fk_overlay (uid) REFERENCES pages_language_overlay(pid)
+) ENGINE = MyISAM DEFAULT CHARACTER SET latin1 COLLATE latin1_german_cs ROW_FORMAT DYNAMIC AUTO_INCREMENT=1;
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/AbstractDataTypeBaseTestCase.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/AbstractDataTypeBaseTestCase.php
new file mode 100644 (file)
index 0000000..f80780e
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement;
+use TYPO3\CMS\Core\Database\Schema\Parser\Parser;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+
+/**
+ * Base class for test cases related to parser data types.
+ */
+abstract class AbstractDataTypeBaseTestCase extends UnitTestCase
+{
+    /**
+     * Insert datatype to test into this create table statement
+     */
+    const CREATE_TABLE_STATEMENT = 'CREATE TABLE `aTable`(`aField` %s);';
+
+    /**
+     * Wrap a column definition into a create table statement for testing
+     *
+     * @param string $columnDefinition
+     * @return string
+     */
+    protected function createTableStatement(string $columnDefinition): string
+    {
+        return sprintf(static::CREATE_TABLE_STATEMENT, $columnDefinition);
+    }
+
+    /**
+     * Parse the CREATE TABLE statement and return the reference definition
+     *
+     * @param string $statement
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem
+     */
+    protected function createSubject(string $statement): CreateColumnDefinitionItem
+    {
+        $parser = new Parser($this->createTableStatement($statement));
+        /** @var CreateTableStatement $createTableStatement */
+        $createTableStatement = $parser->getAST();
+
+        return $createTableStatement->createDefinition->items[0];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/ColumnDefinitionAttributesTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/ColumnDefinitionAttributesTest.php
new file mode 100644 (file)
index 0000000..018d935
--- /dev/null
@@ -0,0 +1,395 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement;
+use TYPO3\CMS\Core\Database\Schema\Parser\Parser;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+
+/**
+ * Tests for CreateColumnDefinitionItem attributes
+ */
+class ColumnDefinitionAttributesTest extends UnitTestCase
+{
+    /**
+     * Each parameter array consists of the following values:
+     *  - column definition attributes SQL fragment
+     *  - allow null values
+     *  - has default value
+     *  - default value
+     *  - auto increment column
+     *  - create index on column
+     *  - create unique index column
+     *  - use column as primary key
+     *  - comment
+     *  - column format
+     *  - storage
+     *
+     * @return array
+     */
+    public function canParseColumnDefinitionAttributesDataProvider(): array
+    {
+        return [
+            'NULL' => [
+                'NULL',
+                true,
+                false,
+                null,
+                false,
+                false,
+                false,
+                false,
+                null,
+                null,
+                null,
+            ],
+            'NOT NULL' => [
+                'NOT NULL',
+                false,
+                false,
+                null,
+                false,
+                false,
+                false,
+                false,
+                null,
+                null,
+                null,
+            ],
+            'DEFAULT' => [
+                "DEFAULT '0'",
+                true,
+                true,
+                '0',
+                false,
+                false,
+                false,
+                false,
+                null,
+                null,
+                null,
+            ],
+            'AUTO_INCREMENT' => [
+                'AUTO_INCREMENT',
+                true,
+                false,
+                null,
+                true,
+                false,
+                false,
+                false,
+                null,
+                null,
+                null,
+            ],
+            'UNIQUE' => [
+                'UNIQUE',
+                true,
+                false,
+                null,
+                false,
+                false,
+                true,
+                false,
+                null,
+                null,
+                null,
+            ],
+            'UNIQUE KEY' => [
+                'UNIQUE KEY',
+                true,
+                false,
+                null,
+                false,
+                false,
+                true,
+                false,
+                null,
+                null,
+                null,
+            ],
+            'PRIMARY' => [
+                'PRIMARY',
+                true,
+                false,
+                null,
+                false,
+                false,
+                false,
+                true,
+                null,
+                null,
+                null,
+            ],
+            'PRIMARY KEY' => [
+                'PRIMARY KEY',
+                true,
+                false,
+                null,
+                false,
+                false,
+                false,
+                true,
+                null,
+                null,
+                null,
+            ],
+            'KEY' => [
+                'KEY',
+                true,
+                false,
+                null,
+                false,
+                true,
+                false,
+                false,
+                null,
+                null,
+                null,
+            ],
+            'COMMENT' => [
+                "COMMENT 'aComment'",
+                true,
+                false,
+                null,
+                false,
+                false,
+                false,
+                false,
+                'aComment',
+                null,
+                null,
+            ],
+            'COLUMN_FORMAT FIXED' => [
+                'COLUMN_FORMAT FIXED',
+                true,
+                false,
+                null,
+                false,
+                false,
+                false,
+                false,
+                null,
+                'fixed',
+                null,
+            ],
+            'COLUMN_FORMAT DYNAMIC' => [
+                'COLUMN_FORMAT DYNAMIC',
+                true,
+                false,
+                null,
+                false,
+                false,
+                false,
+                false,
+                null,
+                'dynamic',
+                null,
+            ],
+            'COLUMN_FORMAT DEFAULT' => [
+                'COLUMN_FORMAT DEFAULT',
+                true,
+                false,
+                null,
+                false,
+                false,
+                false,
+                false,
+                null,
+                null,
+                null,
+            ],
+            'STORAGE DISK' => [
+                'STORAGE DISK',
+                true,
+                false,
+                null,
+                false,
+                false,
+                false,
+                false,
+                null,
+                null,
+                'disk',
+            ],
+            'STORAGE MEMORY' => [
+                'STORAGE MEMORY',
+                true,
+                false,
+                null,
+                false,
+                false,
+                false,
+                false,
+                null,
+                null,
+                'memory',
+            ],
+            'STORAGE DEFAULT' => [
+                'STORAGE DEFAULT',
+                true,
+                false,
+                null,
+                false,
+                false,
+                false,
+                false,
+                null,
+                null,
+                null,
+            ],
+            "NOT NULL DEFAULT '0'" => [
+                "NOT NULL DEFAULT '0'",
+                false,
+                true,
+                '0',
+                false,
+                false,
+                false,
+                false,
+                null,
+                null,
+                null,
+            ],
+            'NOT NULL AUTO_INCREMENT' => [
+                'NOT NULL AUTO_INCREMENT',
+                false,
+                false,
+                null,
+                true,
+                false,
+                false,
+                false,
+                null,
+                null,
+                null,
+            ],
+            'NULL DEFAULT NULL' => [
+                'NULL DEFAULT NULL',
+                true,
+                true,
+                null,
+                false,
+                false,
+                false,
+                false,
+                null,
+                null,
+                null,
+            ],
+            'NOT NULL PRIMARY KEY' => [
+                'NOT NULL PRIMARY KEY',
+                false,
+                false,
+                null,
+                false,
+                false,
+                false,
+                true,
+                null,
+                null,
+                null,
+            ],
+            "NULL DEFAULT 'dummy' UNIQUE" => [
+                "NULL DEFAULT 'dummy' UNIQUE",
+                true,
+                true,
+                'dummy',
+                false,
+                false,
+                true,
+                false,
+                null,
+                null,
+                null,
+            ],
+            "NOT NULL DEFAULT '0' COMMENT 'aComment with blanks' AUTO_INCREMENT PRIMARY KEY COLUMN_FORMAT DYNAMIC" => [
+                "NOT NULL DEFAULT '0' COMMENT 'aComment with blanks' AUTO_INCREMENT PRIMARY KEY COLUMN_FORMAT DYNAMIC",
+                false,
+                true,
+                '0',
+                true,
+                false,
+                false,
+                true,
+                'aComment with blanks',
+                'dynamic',
+                null,
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseColumnDefinitionAttributesDataProvider
+     * @param string $columnAttribute
+     * @param bool $allowNull
+     * @param bool $hasDefaultValue
+     * @param mixed $defaultValue
+     * @param bool $autoIncrement
+     * @param bool $createIndex
+     * @param bool $createUniqueIndex
+     * @param bool $isPrimaryKey
+     * @param string $comment
+     * @param string $columnFormat
+     * @param string $storage
+     */
+    public function canParseColumnDefinitionAttributes(
+        string $columnAttribute,
+        bool $allowNull,
+        bool $hasDefaultValue,
+        $defaultValue,
+        bool $autoIncrement,
+        bool $createIndex,
+        bool $createUniqueIndex,
+        bool $isPrimaryKey,
+        string $comment = null,
+        string $columnFormat = null,
+        string $storage = null
+    ) {
+        $statement = sprintf('CREATE TABLE `aTable`(`aField` INT(11) %s);', $columnAttribute);
+        $subject = $this->createSubject($statement);
+
+        $this->assertInstanceOf(CreateColumnDefinitionItem::class, $subject);
+        $this->assertSame($allowNull, $subject->allowNull);
+        $this->assertSame($hasDefaultValue, $subject->hasDefaultValue);
+        $this->assertSame($defaultValue, $subject->defaultValue);
+        $this->assertSame($createIndex, $subject->index);
+        $this->assertSame($createUniqueIndex, $subject->unique);
+        $this->assertSame($isPrimaryKey, $subject->primary);
+        $this->assertSame($autoIncrement, $subject->autoIncrement);
+        $this->assertSame($comment, $subject->comment);
+        $this->assertSame($columnFormat, $subject->columnFormat);
+        $this->assertSame($storage, $subject->storage);
+    }
+
+    /**
+     * Parse the CREATE TABLE statement and return the reference definition
+     *
+     * @param string $statement
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem
+     */
+    protected function createSubject(string $statement): CreateColumnDefinitionItem
+    {
+        $parser = new Parser($statement);
+        /** @var CreateTableStatement $createTableStatement */
+        $createTableStatement = $parser->getAST();
+
+        return $createTableStatement->createDefinition->items[0];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/ColumnDefinitionItemTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/ColumnDefinitionItemTest.php
new file mode 100644 (file)
index 0000000..555617f
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement;
+use TYPO3\CMS\Core\Database\Schema\Parser\Parser;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+
+/**
+ * Tests for CreateColumnDefinitionItem
+ */
+class ColumnDefinitionItemTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function canParseUnquotedMysqlKeywordAsTableName()
+    {
+        $subject = $this->createSubject('CREATE TABLE `aTable`(checksum VARCHAR(64));');
+
+        $this->assertInstanceOf(CreateColumnDefinitionItem::class, $subject);
+        $this->assertSame($subject->columnName->schemaObjectName, 'checksum');
+    }
+
+    /**
+     * The old regular expression based create table parser processed invalid dump files
+     * where the last column/index definition included a comma before the closing parenthesis.
+     * Emulate this behaviour to avoid breaking lots of (partial) dump files.
+     *
+     * @test
+     */
+    public function canParseCreateDefinitionWithTrailingComma()
+    {
+        $subject = $this->createSubject('CREATE TABLE `aTable`(aField VARCHAR(64), );');
+
+        $this->assertInstanceOf(CreateColumnDefinitionItem::class, $subject);
+    }
+
+    /**
+     * Parse the CREATE TABLE statement and return the reference definition
+     *
+     * @param string $statement
+     * @return \TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateColumnDefinitionItem
+     */
+    protected function createSubject(string $statement): CreateColumnDefinitionItem
+    {
+        $parser = new Parser($statement);
+        /** @var CreateTableStatement $createTableStatement */
+        $createTableStatement = $parser->getAST();
+
+        return $createTableStatement->createDefinition->items[0];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/CreateTableFragmentTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/CreateTableFragmentTest.php
new file mode 100644 (file)
index 0000000..f9b1ff2
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\AbstractCreateStatement;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\CreateTableStatement;
+use TYPO3\CMS\Core\Database\Schema\Parser\Parser;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+
+/**
+ * Tests for CreateTableStatement
+ */
+class CreateTableFragmentTest extends UnitTestCase
+{
+    /**
+     * Each parameter array consists of the following values:
+     *  - create table SQL fragment
+     *  - table name
+     *  - is temporary
+     *
+     * @return array
+     */
+    public function canParseCreateTableFragmentDataProvider(): array
+    {
+        return [
+            'CREATE TABLE' => [
+                'CREATE TABLE aTable (aField INT);',
+                'aTable',
+                false
+            ],
+            'CREATE TEMPORARY TABLE' => [
+                'CREATE TEMPORARY TABLE aTable (aField INT);',
+                'aTable',
+                true
+            ],
+            'CREATE TABLE IF NOT EXISTS' => [
+                'CREATE TABLE IF NOT EXISTS aTable (aField INT);',
+                'aTable',
+                false
+            ],
+            'CREATE TEMPORARY TABLE IF NOT EXISTS' => [
+                'CREATE TEMPORARY TABLE IF NOT EXISTS aTable (aField INT);',
+                'aTable',
+                true
+            ],
+            'CREATE TABLE (quoted table name)' => [
+                'CREATE TABLE `aTable` (aField INT);',
+                'aTable',
+                false
+            ],
+            'CREATE TEMPORARY TABLE (quoted table name)' => [
+                'CREATE TEMPORARY TABLE `aTable` (aField INT);',
+                'aTable',
+                true
+            ],
+            'CREATE TABLE IF NOT EXISTS (quoted table name)' => [
+                'CREATE TABLE IF NOT EXISTS `aTable` (aField INT);',
+                'aTable',
+                false
+            ],
+            'CREATE TEMPORARY TABLE IF NOT EXISTS (quoted table name)' => [
+                'CREATE TEMPORARY TABLE IF NOT EXISTS `aTable` (aField INT);',
+                'aTable',
+                true
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseCreateTableFragmentDataProvider
+     * @param string $statement
+     * @param string $tableName
+     * @param bool $isTemporary
+     */
+    public function canParseCreateTableFragment(string $statement, string $tableName, bool $isTemporary)
+    {
+        $subject = $this->createSubject($statement);
+        $this->assertInstanceOf(CreateTableStatement::class, $subject);
+        $this->assertSame($tableName, $subject->tableName->schemaObjectName);
+        $this->assertSame($isTemporary, $subject->isTemporary);
+    }
+
+    /**
+     * Parse the CREATE TABLE statement and return the reference definition
+     *
+     * @param string $statement
+     * @return AbstractCreateStatement|CreateTableStatement
+     */
+    protected function createSubject(string $statement): AbstractCreateStatement
+    {
+        $parser = new Parser($statement);
+        return $parser->getAST();
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypeAttributes/CharacterTypeAttributesTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypeAttributes/CharacterTypeAttributesTest.php
new file mode 100644 (file)
index 0000000..cd1e0ef
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypeAttributes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * MySQL supports the SQL standard integer types INTEGER (or INT) and SMALLINT.
+ * As an extension to the standard, MySQL also supports the integer types TINYINT, MEDIUMINT, and BIGINT.
+ */
+class CharacterTypeAttributesTest extends AbstractDataTypeBaseTestCase
+{
+
+    /**
+     * Data provider for canParseCharacterDataTypeAttributes()
+     *
+     * @return array
+     */
+    public function canParseCharacterDataTypeAttributesProvider(): array
+    {
+        return [
+            'BINARY' => [
+                'VARCHAR(255) BINARY',
+                ['binary' => true, 'charset' => null, 'collation' => null],
+            ],
+            'CHARACTER SET' => [
+                'TEXT CHARACTER SET latin1',
+                ['binary' => false, 'charset' => 'latin1', 'collation' => null],
+            ],
+            'COLLATE' => [
+                'CHAR(20) COLLATE latin1_german1_ci',
+                ['binary' => false, 'charset' => null, 'collation' => 'latin1_german1_ci'],
+            ],
+            'CHARACTER SET + COLLATE' => [
+                'CHAR(20) CHARACTER SET latin1 COLLATE latin1_german1_ci',
+                ['binary' => false, 'charset' => 'latin1', 'collation' => 'latin1_german1_ci'],
+            ],
+            'BINARY, CHARACTER SET + COLLATE' => [
+                'CHAR(20) BINARY CHARACTER SET latin1 COLLATE latin1_german1_ci',
+                ['binary' => true, 'charset' => 'latin1', 'collation' => 'latin1_german1_ci'],
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseCharacterDataTypeAttributesProvider
+     * @param string $columnDefinition
+     * @param array $options
+     */
+    public function canParseDataType(string $columnDefinition, array $options)
+    {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertSame($options, $subject->dataType->getOptions());
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypeAttributes/EnumerationTypeAttributesTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypeAttributes/EnumerationTypeAttributesTest.php
new file mode 100644 (file)
index 0000000..0032410
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypeAttributes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * MySQL supports the SQL standard integer types INTEGER (or INT) and SMALLINT.
+ * As an extension to the standard, MySQL also supports the integer types TINYINT, MEDIUMINT, and BIGINT.
+ */
+class EnumerationTypeAttributesTest extends AbstractDataTypeBaseTestCase
+{
+
+    /**
+     * Data provider for canParseEnumerationDataTypeAttributes()
+     *
+     * @return array
+     */
+    public function canParseEnumerationDataTypeAttributesProvider(): array
+    {
+        return [
+            'CHARACTER SET' => [
+                "ENUM('value1', 'value2') CHARACTER SET latin1",
+                ['charset' => 'latin1', 'collation' => null],
+            ],
+            'COLLATE' => [
+                "SET('value1', 'value2')  COLLATE latin1_german1_ci",
+                ['charset' => null, 'collation' => 'latin1_german1_ci'],
+            ],
+            'CHARACTER SET + COLLATE' => [
+                "SET('value1', 'value2') CHARACTER SET latin1 COLLATE latin1_german1_ci",
+                ['charset' => 'latin1', 'collation' => 'latin1_german1_ci'],
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseEnumerationDataTypeAttributesProvider
+     * @param string $columnDefinition
+     * @param array $options
+     */
+    public function canParseDataType(string $columnDefinition, array $options)
+    {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertSame($options, $subject->dataType->getOptions());
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypeAttributes/NumericTypeAttributesTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypeAttributes/NumericTypeAttributesTest.php
new file mode 100644 (file)
index 0000000..9c7f41a
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypeAttributes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * MySQL supports the SQL standard integer types INTEGER (or INT) and SMALLINT.
+ * As an extension to the standard, MySQL also supports the integer types TINYINT, MEDIUMINT, and BIGINT.
+ */
+class NumericTypeAttributesTest extends AbstractDataTypeBaseTestCase
+{
+
+    /**
+     * Data provider for canParseNumericDataTypeAttributes()
+     *
+     * @return array
+     */
+    public function canParseNumericDataTypeAttributesProvider(): array
+    {
+        return [
+            'UNSIGNED' => [
+                'INT(11) UNSIGNED',
+                ['unsigned' => true, 'zerofill' => false],
+            ],
+            'ZEROFILL' => [
+                'INT(11) ZEROFILL',
+                ['unsigned' => false, 'zerofill' => true],
+            ],
+            'UNSIGNED ZEROFILL' => [
+                'INT(11) UNSIGNED ZEROFILL',
+                ['unsigned' => true, 'zerofill' => true],
+            ]
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseNumericDataTypeAttributesProvider
+     * @param string $columnDefinition
+     * @param array $options
+     */
+    public function canParseDataType(string $columnDefinition, array $options)
+    {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertSame($options, $subject->dataType->getOptions());
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/BinaryDataTypeTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/BinaryDataTypeTest.php
new file mode 100644 (file)
index 0000000..05d8ef6
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\BinaryDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\VarBinaryDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\Parser;
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * Tests for parsing BINARY/VARBINARY SQL data types
+ */
+class BinaryDataTypeTest extends AbstractDataTypeBaseTestCase
+{
+    /**
+     * Data provider for canParseBinaryDataType()
+     *
+     * @return array
+     */
+    public function canParseBinaryDataTypeProvider(): array
+    {
+        return [
+            'BINARY without length' => [
+                'BINARY',
+                BinaryDataType::class,
+                0,
+            ],
+            'BINARY with length' => [
+                'BINARY(200)',
+                BinaryDataType::class,
+                200,
+            ],
+            'VARBINARY with length' => [
+                'VARBINARY(200)',
+                VarBinaryDataType::class,
+                200,
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseBinaryDataTypeProvider
+     * @param string $columnDefinition
+     * @param string $className
+     * @param int $length
+     */
+    public function canParseDataType(string $columnDefinition, string $className, int $length)
+    {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertInstanceOf($className, $subject->dataType);
+        $this->assertSame($length, $subject->dataType->getLength());
+    }
+
+    /**
+     * @test
+     */
+    public function lengthIsRequiredForVarBinaryType()
+    {
+        $this->expectExceptionCode(1471504822);
+        $this->expectExceptionMessageRegExp('Error: The current data type requires a field length definition');
+        new Parser('CREATE TABLE `aTable`(`aField` VARBINARY);');
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/BitDataTypeTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/BitDataTypeTest.php
new file mode 100644 (file)
index 0000000..5a329ce
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\BitDataType;
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * Tests for parsing BIT SQL data type
+ */
+class BitDataTypeTest extends AbstractDataTypeBaseTestCase
+{
+    /**
+     * Data provider for canParseBitDataType()
+     *
+     * @return array
+     */
+    public function canParseBitDataTypeProvider(): array
+    {
+        return [
+            'BIT without length' => [
+                'BIT',
+                BitDataType::class,
+                0,
+            ],
+            'BIT with length' => [
+                'BIT(8)',
+                BitDataType::class,
+                8,
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseBitDataTypeProvider
+     * @param string $columnDefinition
+     * @param string $className
+     * @param int $length
+     */
+    public function canParseDataType(string $columnDefinition, string $className, int $length)
+    {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertInstanceOf($className, $subject->dataType);
+        $this->assertSame($length, $subject->dataType->getLength());
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/BlobTypesTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/BlobTypesTest.php
new file mode 100644 (file)
index 0000000..0fec00d
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\BlobDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\LongBlobDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\MediumBlobDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TinyBlobDataType;
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * Tests for parsing BLOB SQL data types
+ */
+class BlobTypesTest extends AbstractDataTypeBaseTestCase
+{
+    /**
+     * Data provider for canParseBlobDataType()
+     *
+     * @return array
+     */
+    public function canParseBlobDataTypeProvider(): array
+    {
+        return [
+            'TINYBLOB' => [
+                'TINYBLOB',
+                TinyBlobDataType::class,
+            ],
+            'BLOB' => [
+                'BLOB',
+                BlobDataType::class,
+            ],
+            'MEDIUMBLOB' => [
+                'MEDIUMBLOB',
+                MediumBlobDataType::class,
+            ],
+            'LONGBLOB' => [
+                'LONGBLOB',
+                LongBlobDataType::class,
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseBlobDataTypeProvider
+     * @param string $columnDefinition
+     * @param string $className
+     */
+    public function canParseDataType(string $columnDefinition, string $className)
+    {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertInstanceOf($className, $subject->dataType);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/CharDataTypeTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/CharDataTypeTest.php
new file mode 100644 (file)
index 0000000..632db88
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\CharDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\VarCharDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\Parser;
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * Tests for parsing CHAR/VARCHAR SQL data types
+ */
+class CharDataTypeTest extends AbstractDataTypeBaseTestCase
+{
+    /**
+     * Data provider for canParseBinaryDataType()
+     *
+     * @return array
+     */
+    public function canParseBinaryDataTypeProvider(): array
+    {
+        return [
+            'CHAR without length' => [
+                'CHAR',
+                CharDataType::class,
+                0,
+            ],
+            'CHAR with length' => [
+                'CHAR(200)',
+                CharDataType::class,
+                200,
+            ],
+            'VARCHAR with length' => [
+                'VARCHAR(200)',
+                VarCharDataType::class,
+                200,
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseBinaryDataTypeProvider
+     * @param string $columnDefinition
+     * @param string $className
+     * @param int $length
+     */
+    public function canParseDataType(string $columnDefinition, string $className, int $length)
+    {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertInstanceOf($className, $subject->dataType);
+        $this->assertSame($length, $subject->dataType->getLength());
+    }
+
+    /**
+     * @test
+     */
+    public function lengthIsRequiredForVarCharType()
+    {
+        $this->expectExceptionCode(1471504822);
+        $this->expectExceptionMessageRegExp('Error: The current data type requires a field length definition');
+        new Parser('CREATE TABLE `aTable`(`aField` VARCHAR);');
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/DateTimeTypesTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/DateTimeTypesTest.php
new file mode 100644 (file)
index 0000000..9692b98
--- /dev/null
@@ -0,0 +1,125 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\DateDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\DateTimeDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TimeDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TimestampDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\YearDataType;
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * Tests for parsing DATE/TIME related SQL data types
+ */
+class DateTimeTypesTest extends AbstractDataTypeBaseTestCase
+{
+    /**
+     * Data provider for canParseDateTimeType()
+     *
+     * @return array
+     */
+    public function canParseDateTimeTypeProvider(): array
+    {
+        return [
+            'DATE' => [
+                'DATE',
+                DateDataType::class,
+                null,
+            ],
+            'YEAR' => [
+                'YEAR',
+                YearDataType::class,
+                null,
+            ],
+            'TIME' => [
+                'TIME',
+                TimeDataType::class,
+                0,
+            ],
+            'TIME with fractional second part' => [
+                'TIME(3)',
+                TimeDataType::class,
+                3,
+            ],
+            'TIMESTAMP' => [
+                'TIMESTAMP',
+                TimestampDataType::class,
+                0,
+            ],
+            'TIMESTAMP with fractional second part' => [
+                'TIMESTAMP(3)',
+                TimestampDataType::class,
+                3,
+            ],
+            'DATETIME' => [
+                'DATETIME',
+                DateTimeDataType::class,
+                0,
+            ],
+            'DATETIME with fractional second part' => [
+                'DATETIME(3)',
+                DateTimeDataType::class,
+                3,
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseDateTimeTypeProvider
+     * @param string $columnDefinition
+     * @param string $className
+     * @param int $length
+     */
+    public function canParseDataType(string $columnDefinition, string $className, int $length = null)
+    {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertInstanceOf($className, $subject->dataType);
+
+        // DATE & YEAR don't support fractional second parts
+        if ($length !== null) {
+            $this->assertSame($length, $subject->dataType->getLength());
+        }
+    }
+
+    /**
+     * @test
+     */
+    public function parseDateTimeTypeWithInvalidLowerBound()
+    {
+        $this->expectException(StatementException::class);
+        $this->expectExceptionMessageRegExp(
+            '@Error: the fractional seconds part for TIME, DATETIME or TIMESTAMP columns must >= 0@'
+        );
+        $this->createSubject('TIME(-1)');
+    }
+
+    /**
+     * @test
+     */
+    public function parseDateTimeTypeWithInvalidUpperBound()
+    {
+        $this->expectException(StatementException::class);
+        $this->expectExceptionMessageRegExp(
+            '@Error: the fractional seconds part for TIME, DATETIME or TIMESTAMP columns must <= 6@'
+        );
+        $this->createSubject('TIME(7)');
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/EnumDataTypeTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/EnumDataTypeTest.php
new file mode 100644 (file)
index 0000000..6294ccd
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\EnumDataType;
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * Tests for parsing ENUM SQL data type
+ */
+class EnumDataTypeTest extends AbstractDataTypeBaseTestCase
+{
+    /**
+     * Data provider for canParseEnumDataType()
+     *
+     * @return array
+     */
+    public function canParseEnumDataTypeProvider(): array
+    {
+        return [
+            'ENUM(value)' => [
+                "ENUM('value1')",
+                EnumDataType::class,
+                ['value1'],
+            ],
+            'ENUM(value,value)' => [
+                "ENUM('value1','value2')",
+                EnumDataType::class,
+                ['value1', 'value2'],
+            ],
+            'ENUM(value, value)' => [
+                "ENUM('value1', 'value2')",
+                EnumDataType::class,
+                ['value1', 'value2'],
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseEnumDataTypeProvider
+     * @param string $columnDefinition
+     * @param string $className
+     * @param array $values
+     */
+    public function canParseDataType(string $columnDefinition, string $className, array $values)
+    {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertInstanceOf($className, $subject->dataType);
+        $this->assertSame($values, $subject->dataType->getValues());
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/FixedPointTypesTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/FixedPointTypesTest.php
new file mode 100644 (file)
index 0000000..6b17405
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\DecimalDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\NumericDataType;
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * Tests for parsing DECIMAL/NUMERIC SQL data types
+ */
+class FixedPointTypesTest extends AbstractDataTypeBaseTestCase
+{
+    /**
+     * Data provider for canParseFixedPointTypes()
+     *
+     * @return array
+     */
+    public function canParseFixedPointTypesProvider(): array
+    {
+        return [
+            'DECIMAL without precision and scale' => [
+                'DECIMAL',
+                DecimalDataType::class,
+                -1,
+                -1,
+            ],
+            'DECIMAL with precision' => [
+                'DECIMAL(5)',
+                DecimalDataType::class,
+                5,
+                -1,
+            ],
+            'DECIMAL with precision and scale' => [
+                'DECIMAL(5,2)',
+                DecimalDataType::class,
+                5,
+                2,
+            ],
+            'NUMERIC without length' => [
+                'NUMERIC',
+                NumericDataType::class,
+                -1,
+                -1,
+            ],
+            'NUMERIC with length' => [
+                'NUMERIC(5)',
+                NumericDataType::class,
+                5,
+                -1,
+            ],
+            'NUMERIC with length and precision' => [
+                'NUMERIC(5,2)',
+                NumericDataType::class,
+                5,
+                2,
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseFixedPointTypesProvider
+     * @param string $columnDefinition
+     * @param string $className
+     * @param int $precision
+     * @param int $scale
+     */
+    public function canParseDataType(
+        string $columnDefinition,
+        string $className,
+        int $precision = null,
+        int $scale = null
+    ) {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertInstanceOf($className, $subject->dataType);
+        $this->assertSame($precision, $subject->dataType->getPrecision());
+        $this->assertSame($scale, $subject->dataType->getScale());
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/FloatingPointTypesTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/FloatingPointTypesTest.php
new file mode 100644 (file)
index 0000000..d4eef0a
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\DoubleDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\FloatDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\RealDataType;
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * Tests for parsing FLOAT/REAL/DOUBLE SQL data types
+ */
+class FloatingPointTypesTest extends AbstractDataTypeBaseTestCase
+{
+    /**
+     * Data provider for canParseFloatingPointTypes()
+     *
+     * @return array
+     */
+    public function canParseFloatingPointTypesProvider(): array
+    {
+        return [
+            'FLOAT without precision' => [
+                'FLOAT',
+                FloatDataType::class,
+                -1,
+                -1,
+            ],
+            'FLOAT with precision' => [
+                'FLOAT(44)',
+                FloatDataType::class,
+                44,
+                -1,
+            ],
+            'FLOAT with precision and decimals' => [
+                'FLOAT(44,5)',
+                FloatDataType::class,
+                44,
+                5,
+            ],
+            'REAL without precision' => [
+                'REAL',
+                RealDataType::class,
+                -1,
+                -1,
+            ],
+            'REAL with precision' => [
+                'REAL(44)',
+                RealDataType::class,
+                44,
+                -1,
+            ],
+            'REAL with precision and decimals' => [
+                'REAL(44,5)',
+                RealDataType::class,
+                44,
+                5,
+            ],
+            'DOUBLE without precision' => [
+                'DOUBLE',
+                DoubleDataType::class,
+                -1,
+                -1,
+            ],
+            'DOUBLE with precision' => [
+                'DOUBLE(44)',
+                DoubleDataType::class,
+                44,
+                -1,
+            ],
+            'DOUBLE with precision and decimals' => [
+                'DOUBLE(44,5)',
+                DoubleDataType::class,
+                44,
+                5,
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseFloatingPointTypesProvider
+     * @param string $columnDefinition
+     * @param string $className
+     * @param int $precision
+     * @param int $scale
+     */
+    public function canParseDataType(
+        string $columnDefinition,
+        string $className,
+        int $precision = null,
+        int $scale = null
+    ) {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertInstanceOf($className, $subject->dataType);
+        $this->assertSame($precision, $subject->dataType->getPrecision());
+        $this->assertSame($scale, $subject->dataType->getScale());
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/IntegerTypesTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/IntegerTypesTest.php
new file mode 100644 (file)
index 0000000..fdce0ea
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\BigIntDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\IntegerDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\MediumIntDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\SmallIntDataType;
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\TinyIntDataType;
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * Tests for parsing INTEGER SQL data types
+ */
+class IntegerTypesTest extends AbstractDataTypeBaseTestCase
+{
+    /**
+     * Data provider for canParseIntegerDataType()
+     *
+     * @return array
+     */
+    public function canParseIntegerDataTypeProvider(): array
+    {
+        return [
+            'TINYINT without length' => [
+                'TINYINT',
+                TinyIntDataType::class,
+                0,
+            ],
+            'SMALLINT without length' => [
+                'SMALLINT',
+                SmallIntDataType::class,
+                0,
+            ],
+            'MEDIUMINT without length' => [
+                'MEDIUMINT',
+                MediumIntDataType::class,
+                0,
+            ],
+            'INT without length' => [
+                'INT',
+                IntegerDataType::class,
+                0,
+            ],
+            'INTEGER without length' => [
+                'INTEGER',
+                IntegerDataType::class,
+                0,
+            ],
+            'BIGINT without length' => [
+                'BIGINT',
+                BigIntDataType::class,
+                0,
+            ],
+            // MySQL supports an extension for optionally specifying the display width of integer data types
+            // in parentheses following the base keyword for the type. For example, INT(4) specifies an INT
+            // with a display width of four digits.
+            // The display width does not constrain the range of values that can be stored in the column.
+            'TINYINT with length' => [
+                'TINYINT(4)',
+                TinyIntDataType::class,
+                4,
+            ],
+            'SMALLINT with length' => [
+                'SMALLINT(6)',
+                SmallIntDataType::class,
+                6,
+            ],
+            'MEDIUMINT with length' => [
+                'MEDIUMINT(8)',
+                MediumIntDataType::class,
+                8,
+            ],
+            'INT with length' => [
+                'INT(11)',
+                IntegerDataType::class,
+                11,
+            ],
+            'INTEGER with length' => [
+                'INTEGER(11)',
+                IntegerDataType::class,
+                11,
+            ],
+            'BIGINT with length' => [
+                'BIGINT(20)',
+                BigIntDataType::class,
+                20,
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseIntegerDataTypeProvider
+     * @param string $columnDefinition
+     * @param string $className
+     * @param int $length
+     */
+    public function canParseDataType(string $columnDefinition, string $className, int $length)
+    {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertInstanceOf($className, $subject->dataType);
+        $this->assertSame($length, $subject->dataType->getLength());
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/JsonDataTypeTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/JsonDataTypeTest.php
new file mode 100644 (file)
index 0000000..4d00ca1
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\JsonDataType;
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * Tests for parsing JSON SQL data type
+ */
+class JsonDataTypeTest extends AbstractDataTypeBaseTestCase
+{
+    /**
+     * @test
+     */
+    public function canParseBitDataType()
+    {
+        $subject = $this->createSubject('JSON');
+
+        $this->assertInstanceOf(JsonDataType::class, $subject->dataType);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/SetDataTypeTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/SetDataTypeTest.php
new file mode 100644 (file)
index 0000000..9c3266b
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\DataTypes;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Schema\Parser\AST\DataType\SetDataType;
+use TYPO3\CMS\Core\Tests\Unit\Database\Schema\Parser\AbstractDataTypeBaseTestCase;
+
+/**
+ * Tests for parsing SET SQL data type
+ */
+class SetDataTypeTest extends AbstractDataTypeBaseTestCase
+{
+    /**
+     * Data provider for canParseSetDataType()
+     *
+     * @return array
+     */
+    public function canParseSetDataTypeProvider(): array
+    {
+        return [
+            'SET(value)' => [
+                "SET('value1')",
+                SetDataType::class,
+                ['value1'],
+            ],
+            'SET(value,value)' => [
+                "SET('value1','value2')",
+                SetDataType::class,
+                ['value1', 'value2'],
+            ],
+            'SET(value, value)' => [
+                "SET('value1', 'value2')",
+                SetDataType::class,
+                ['value1', 'value2'],
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider canParseSetDataTypeProvider
+     * @param string $columnDefinition
+     * @param string $className
+     * @param array $values
+     */
+    public function canParseDataType(string $columnDefinition, string $className, array $values)
+    {
+        $subject = $this->createSubject($columnDefinition);
+
+        $this->assertInstanceOf($className, $subject->dataType);
+        $this->assertSame($values, $subject->dataType->getValues());
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/TextTypesTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/Parser/DataTypes/TextTypesTest.php
new file mode 100644 (file)
index 0000000..d14b257
--- /dev/null
+++ b/