Skip to content

Commit

Permalink
Implement an EnumType for MySQL/MariaDB (#6536)
Browse files Browse the repository at this point in the history
|      Q       |   A
|------------- | -----------
| Type         | feature
| Fixed issues | doctrine/migrations#1441 (partly)

#### Summary

This PR adds an `EnumType` that allows us to introspect and diff tables
that make use of MySQL's `ENUM` column type.
  • Loading branch information
derrabus authored Oct 10, 2024
1 parent 54be50e commit 9744af4
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 12 deletions.
10 changes: 10 additions & 0 deletions docs/en/reference/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@ type natively, this type is mapped to the ``string`` type internally.
Values retrieved from the database are always converted to PHP's ``string`` type
or ``null`` if no data is present.

enum
++++

Maps and converts a string which is one of a set of predefined values. This
type is specifically designed for MySQL and MariaDB, where it is mapped to
the native ``ENUM`` type. For other database vendors, this type is mapped to
a string field (``VARCHAR``) with the maximum length being the length of the
longest value in the set. Values retrieved from the database are always
converted to PHP's ``string`` type or ``null`` if no data is present.

Binary string types
^^^^^^^^^^^^^^^^^^^

Expand Down
30 changes: 30 additions & 0 deletions src/Exception/InvalidColumnType/ColumnValuesRequired.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Exception\InvalidColumnType;

use Doctrine\DBAL\Exception\InvalidColumnType;
use Doctrine\DBAL\Platforms\AbstractPlatform;

use function get_debug_type;
use function sprintf;

/** @psalm-immutable */
final class ColumnValuesRequired extends InvalidColumnType
{
/**
* @param AbstractPlatform $platform The target platform
* @param string $type The SQL column type
*/
public static function new(AbstractPlatform $platform, string $type): self
{
return new self(
sprintf(
'%s requires the values of a %s column to be specified',
get_debug_type($platform),
$type,
),
);
}
}
19 changes: 19 additions & 0 deletions src/Platforms/AbstractMySQLPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Exception\InvalidColumnType\ColumnValuesRequired;
use Doctrine\DBAL\Platforms\Keywords\KeywordList;
use Doctrine\DBAL\Platforms\Keywords\MySQLKeywords;
use Doctrine\DBAL\Schema\AbstractAsset;
Expand All @@ -18,12 +19,14 @@
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types\Types;

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 ColumnValuesRequired::new($this, 'ENUM');
}

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
22 changes: 22 additions & 0 deletions src/Platforms/AbstractPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Doctrine\DBAL\Exception\InvalidColumnType\ColumnLengthRequired;
use Doctrine\DBAL\Exception\InvalidColumnType\ColumnPrecisionRequired;
use Doctrine\DBAL\Exception\InvalidColumnType\ColumnScaleRequired;
use Doctrine\DBAL\Exception\InvalidColumnType\ColumnValuesRequired;
use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\Platforms\Exception\NoColumnsSpecifiedForTable;
use Doctrine\DBAL\Platforms\Exception\NotSupported;
Expand Down Expand Up @@ -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,25 @@ 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<string, mixed> $column
*
* @throws ColumnValuesRequired If the column definition does not contain any values.
*/
public function getEnumDeclarationSQL(array $column): string
{
if (! isset($column['values']) || ! is_array($column['values']) || $column['values'] === []) {
throw ColumnValuesRequired::new($this, 'ENUM');
}

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);
}
}
21 changes: 21 additions & 0 deletions src/Schema/MySQLSchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
use Doctrine\DBAL\Types\Type;

use function array_change_key_case;
use function array_map;
use function assert;
use function explode;
use function implode;
use function is_string;
use function preg_match;
use function preg_match_all;
use function str_contains;
use function strtok;
use function strtolower;
Expand Down Expand Up @@ -134,6 +136,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 +196,10 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
case 'year':
$length = null;
break;

case 'enum':
$values = $this->parseEnumExpression($tableColumn['type']);
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 All @@ -228,6 +237,18 @@ protected function _getPortableTableColumnDefinition(array $tableColumn): Column
return $column;
}

/** @return list<string> */
private function parseEnumExpression(string $expression): array
{
$result = preg_match_all("/'([^']*(?:''[^']*)*)'/", $expression, $matches);
assert($result !== false);

return array_map(
static fn (string $match): string => strtr($match, ["''" => "'"]),
$matches[1],
);
}

/**
* Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers.
*
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
Loading

0 comments on commit 9744af4

Please sign in to comment.