Skip to content

Commit

Permalink
Implement an EnumType for MySQL/MariaDB
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabus committed Oct 9, 2024
1 parent 79b257b commit be4bcc4
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 12 deletions.
19 changes: 19 additions & 0 deletions src/Platforms/AbstractMySQLPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions src/Platforms/AbstractPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 espacially popular in MySQL and MariaDB. By default, this method throws
* because we assume that the platform does not support ENUM types.
*
* @param array<string, mixed> $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.
*
Expand Down
44 changes: 33 additions & 11 deletions src/Schema/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class Column extends AbstractAsset

protected bool $_autoincrement = false;

/** @var list<string> */
protected array $_values = [];

/** @var array<string, mixed> */
protected array $_platformOptions = [];

Expand Down Expand Up @@ -231,22 +234,41 @@ public function getComment(): string
return $this->_comment;
}

/**
* @param list<string> $values
*
* @return $this
*/
public function setValues(array $values): static
{
$this->_values = $values;

return $this;
}

/** @return list<string> */
public function getValues(): array
{
return $this->_values;
}

/** @return array<string, mixed> */
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);
}
}
9 changes: 9 additions & 0 deletions src/Schema/MySQLSchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use function strtok;
use function strtolower;
use function strtr;
use function substr;

use const CASE_LOWER;

Expand Down Expand Up @@ -134,6 +135,8 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column

$type = $this->platform->getDoctrineTypeMapping($dbType);

$values = [];

switch ($dbType) {
case 'char':
case 'binary':
Expand Down Expand Up @@ -192,6 +195,11 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
case 'year':
$length = null;
break;

case 'enum':
case 'set':
$values = explode('\',\'', substr($tableColumn['type'], 6, -2));
break;
}

if ($this->platform instanceof MariaDBPlatform) {
Expand All @@ -209,6 +217,7 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
'scale' => $scale,
'precision' => $precision,
'autoincrement' => str_contains($tableColumn['extra'], 'auto_increment'),
'values' => $values,
];

if (isset($tableColumn['comment'])) {
Expand Down
18 changes: 18 additions & 0 deletions src/Types/EnumType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;

final class EnumType extends Type
{
/**
* {@inheritDoc}
*/
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return $platform->getEnumDeclarationSQL($column);
}
}
1 change: 1 addition & 0 deletions src/Types/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/Types/Types.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 4 additions & 1 deletion tests/Functional/Schema/MySQLSchemaManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
75 changes: 75 additions & 0 deletions tests/Functional/Types/EnumTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Functional\Types;

use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Types\EnumType;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;

final class EnumTypeTest extends FunctionalTestCase
{
protected function setUp(): void
{
try {
$this->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)]);

Check warning on line 68 in tests/Functional/Types/EnumTypeTest.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (PHP: 8.3)

Line exceeds 120 characters; contains 126 characters

self::assertSame(
[['id' => 1, 'suit' => 'hearts'], ['id' => 2, 'suit' => 'diamonds']],
$this->connection->fetchAllAssociative('SELECT id, suit FROM my_enum_table'),
);
}
}
1 change: 1 addition & 0 deletions tests/Schema/ColumnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public function testToArray(): void
'autoincrement' => false,
'columnDefinition' => null,
'comment' => '',
'values' => [],
'foo' => 'bar',
];

Expand Down

0 comments on commit be4bcc4

Please sign in to comment.