diff --git a/src/Platforms/AbstractMySQLPlatform.php b/src/Platforms/AbstractMySQLPlatform.php index 55cebe7ae9f..f36e71a082e 100644 --- a/src/Platforms/AbstractMySQLPlatform.php +++ b/src/Platforms/AbstractMySQLPlatform.php @@ -17,13 +17,16 @@ use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder; use Doctrine\DBAL\TransactionIsolationLevel; use Doctrine\DBAL\Types\Types; +use RuntimeException; +use function array_map; use function array_merge; use function array_unique; use function array_values; use function count; use function implode; use function in_array; +use function is_array; use function is_numeric; use function sprintf; use function str_replace; @@ -645,6 +648,21 @@ public function getDecimalTypeDeclarationSQL(array $column): string return parent::getDecimalTypeDeclarationSQL($column) . $this->getUnsignedDeclaration($column); } + /** + * {@inheritDoc} + */ + public function getEnumDeclarationSQL(array $column): string + { + if (! isset($column['values']) || ! is_array($column['values']) || $column['values'] === []) { + throw new RuntimeException('Incomplete ENUM column definition. ENUM columns require an array of values.'); + } + + return sprintf('ENUM(%s)', implode(', ', array_map( + $this->quoteStringLiteral(...), + $column['values'], + ))); + } + /** * Get unsigned declaration for a column. * @@ -718,6 +736,7 @@ protected function initializeDoctrineTypeMappings(): void 'datetime' => Types::DATETIME_MUTABLE, 'decimal' => Types::DECIMAL, 'double' => Types::FLOAT, + 'enum' => Types::ENUM, 'float' => Types::SMALLFLOAT, 'int' => Types::INTEGER, 'integer' => Types::INTEGER, diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php index d7fe9a790f8..cdaf2915d99 100644 --- a/src/Platforms/AbstractPlatform.php +++ b/src/Platforms/AbstractPlatform.php @@ -35,6 +35,7 @@ use Doctrine\DBAL\Types; use Doctrine\DBAL\Types\Exception\TypeNotFound; use Doctrine\DBAL\Types\Type; +use RuntimeException; use function addcslashes; use function array_map; @@ -51,6 +52,8 @@ use function is_float; use function is_int; use function is_string; +use function max; +use function mb_strlen; use function preg_quote; use function preg_replace; use function sprintf; @@ -190,6 +193,23 @@ public function getBinaryTypeDeclarationSQL(array $column): string } } + /** + * Returns the SQL snippet to declare an ENUM column. + * + * Enum is a non-standard type that is especially popular in MySQL and MariaDB. By default, this method map to + * a simple VARCHAR field which allows us to deploy it on any platform, e.g. SQLite. + * + * @param array $column + */ + public function getEnumDeclarationSQL(array $column): string + { + if (! isset($column['values']) || ! is_array($column['values']) || $column['values'] === []) { + throw new RuntimeException('Incomplete ENUM column definition. ENUM columns require an array of values.'); + } + + return $this->getStringTypeDeclarationSQL(['length' => max(...array_map(mb_strlen(...), $column['values']))]); + } + /** * Returns the SQL snippet to declare a GUID/UUID column. * diff --git a/src/Schema/Column.php b/src/Schema/Column.php index 8963cd7acb2..2d02403b262 100644 --- a/src/Schema/Column.php +++ b/src/Schema/Column.php @@ -33,6 +33,9 @@ class Column extends AbstractAsset protected bool $_autoincrement = false; + /** @var list */ + protected array $_values = []; + /** @var array */ protected array $_platformOptions = []; @@ -231,22 +234,41 @@ public function getComment(): string return $this->_comment; } + /** + * @param list $values + * + * @return $this + */ + public function setValues(array $values): static + { + $this->_values = $values; + + return $this; + } + + /** @return list */ + public function getValues(): array + { + return $this->_values; + } + /** @return array */ public function toArray(): array { return array_merge([ - 'name' => $this->_name, - 'type' => $this->_type, - 'default' => $this->_default, - 'notnull' => $this->_notnull, - 'length' => $this->_length, - 'precision' => $this->_precision, - 'scale' => $this->_scale, - 'fixed' => $this->_fixed, - 'unsigned' => $this->_unsigned, - 'autoincrement' => $this->_autoincrement, + 'name' => $this->_name, + 'type' => $this->_type, + 'default' => $this->_default, + 'notnull' => $this->_notnull, + 'length' => $this->_length, + 'precision' => $this->_precision, + 'scale' => $this->_scale, + 'fixed' => $this->_fixed, + 'unsigned' => $this->_unsigned, + 'autoincrement' => $this->_autoincrement, 'columnDefinition' => $this->_columnDefinition, - 'comment' => $this->_comment, + 'comment' => $this->_comment, + 'values' => $this->_values, ], $this->_platformOptions); } } diff --git a/src/Schema/MySQLSchemaManager.php b/src/Schema/MySQLSchemaManager.php index 1c0915905fa..24f05269b77 100644 --- a/src/Schema/MySQLSchemaManager.php +++ b/src/Schema/MySQLSchemaManager.php @@ -26,6 +26,7 @@ use function strtok; use function strtolower; use function strtr; +use function substr; use const CASE_LOWER; @@ -134,6 +135,8 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column $type = $this->platform->getDoctrineTypeMapping($dbType); + $values = []; + switch ($dbType) { case 'char': case 'binary': @@ -192,6 +195,10 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column case 'year': $length = null; break; + + case 'enum': + $values = explode('\',\'', substr($tableColumn['type'], 6, -2)); + break; } if ($this->platform instanceof MariaDBPlatform) { @@ -209,6 +216,7 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column 'scale' => $scale, 'precision' => $precision, 'autoincrement' => str_contains($tableColumn['extra'], 'auto_increment'), + 'values' => $values, ]; if (isset($tableColumn['comment'])) { diff --git a/src/Types/EnumType.php b/src/Types/EnumType.php new file mode 100644 index 00000000000..489dc4b5c7d --- /dev/null +++ b/src/Types/EnumType.php @@ -0,0 +1,18 @@ +getEnumDeclarationSQL($column); + } +} diff --git a/src/Types/Type.php b/src/Types/Type.php index bc4d3aaf417..ee7797f0960 100644 --- a/src/Types/Type.php +++ b/src/Types/Type.php @@ -34,6 +34,7 @@ abstract class Type Types::DATETIMETZ_MUTABLE => DateTimeTzType::class, Types::DATETIMETZ_IMMUTABLE => DateTimeTzImmutableType::class, Types::DECIMAL => DecimalType::class, + Types::ENUM => EnumType::class, Types::FLOAT => FloatType::class, Types::GUID => GuidType::class, Types::INTEGER => IntegerType::class, diff --git a/src/Types/Types.php b/src/Types/Types.php index 6fef4cfce08..319218b021a 100644 --- a/src/Types/Types.php +++ b/src/Types/Types.php @@ -23,6 +23,7 @@ final class Types public const DATETIMETZ_IMMUTABLE = 'datetimetz_immutable'; public const DECIMAL = 'decimal'; public const FLOAT = 'float'; + public const ENUM = 'enum'; public const GUID = 'guid'; public const INTEGER = 'integer'; public const JSON = 'json'; diff --git a/tests/Functional/Schema/MySQLSchemaManagerTest.php b/tests/Functional/Schema/MySQLSchemaManagerTest.php index 852eb555423..893f673ca3a 100644 --- a/tests/Functional/Schema/MySQLSchemaManagerTest.php +++ b/tests/Functional/Schema/MySQLSchemaManagerTest.php @@ -561,7 +561,10 @@ public function testColumnIntrospection(): void $doctrineTypes = array_keys(Type::getTypesMap()); foreach ($doctrineTypes as $type) { - $table->addColumn('col_' . $type, $type, ['length' => 8, 'precision' => 8, 'scale' => 2]); + $table->addColumn('col_' . $type, $type, match ($type) { + Types::ENUM => ['values' => ['foo', 'bar']], + default => ['length' => 8, 'precision' => 8, 'scale' => 2], + }); } $this->dropAndCreateTable($table); diff --git a/tests/Functional/Types/EnumTypeTest.php b/tests/Functional/Types/EnumTypeTest.php new file mode 100644 index 00000000000..8dffab18e98 --- /dev/null +++ b/tests/Functional/Types/EnumTypeTest.php @@ -0,0 +1,79 @@ +connection->createSchemaManager()->dropTable('my_enum_table'); + } catch (TableNotFoundException) { + } + } + + public function testIntrospectEnum(): void + { + if (! $this->connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { + self::markTestSkipped('This test requires MySQL or MariaDB.'); + } + + $this->connection->executeStatement(<<< 'SQL' + CREATE TABLE my_enum_table ( + id BIGINT NOT NULL PRIMARY KEY, + suit ENUM('hearts', 'diamonds', 'clubs', 'spades') NOT NULL DEFAULT 'hearts' + ); + SQL); + + $schemaManager = $this->connection->createSchemaManager(); + $table = $schemaManager->introspectTable('my_enum_table'); + + self::assertCount(2, $table->getColumns()); + self::assertTrue($table->hasColumn('suit')); + self::assertInstanceOf(EnumType::class, $table->getColumn('suit')->getType()); + self::assertSame(['hearts', 'diamonds', 'clubs', 'spades'], $table->getColumn('suit')->getValues()); + self::assertSame('hearts', $table->getColumn('suit')->getDefault()); + } + + public function testDeployEnum(): void + { + $schemaManager = $this->connection->createSchemaManager(); + $schema = new Schema(schemaConfig: $schemaManager->createSchemaConfig()); + $table = $schema->createTable('my_enum_table'); + $table->addColumn('id', Types::BIGINT, ['notnull' => true]); + $table->addColumn('suit', Types::ENUM, [ + 'values' => ['hearts', 'diamonds', 'clubs', 'spades'], + 'notnull' => true, + 'default' => 'hearts', + ]); + $table->setPrimaryKey(['id']); + + $schemaManager->createSchemaObjects($schema); + + $introspectedTable = $schemaManager->introspectTable('my_enum_table'); + + self::assertTrue($schemaManager->createComparator()->compareTables($table, $introspectedTable)->isEmpty()); + + $this->connection->insert('my_enum_table', ['id' => 1, 'suit' => 'hearts'], ['suit' => Types::ENUM]); + $this->connection->insert( + 'my_enum_table', + ['id' => 2, 'suit' => 'diamonds'], + ['suit' => Type::getType(Types::ENUM)], + ); + + self::assertEquals( + [[1, 'hearts'], [2, 'diamonds']], + $this->connection->fetchAllNumeric('SELECT id, suit FROM my_enum_table ORDER BY id ASC'), + ); + } +} diff --git a/tests/Schema/ColumnTest.php b/tests/Schema/ColumnTest.php index 4e99bba7ba2..c10bdd7be3e 100644 --- a/tests/Schema/ColumnTest.php +++ b/tests/Schema/ColumnTest.php @@ -52,6 +52,7 @@ public function testToArray(): void 'autoincrement' => false, 'columnDefinition' => null, 'comment' => '', + 'values' => [], 'foo' => 'bar', ];