From 703abc8cb7ed5ff29045bcee7591225cd93512a5 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 21 Nov 2025 17:43:42 +0300 Subject: [PATCH 01/13] start --- src/Column/ColumnDefinitionBuilder.php | 1 + src/Column/ColumnFactory.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Column/ColumnDefinitionBuilder.php b/src/Column/ColumnDefinitionBuilder.php index 26bb1c45d..c75a1c33e 100644 --- a/src/Column/ColumnDefinitionBuilder.php +++ b/src/Column/ColumnDefinitionBuilder.php @@ -122,6 +122,7 @@ protected function getDbType(ColumnInterface $column): string ColumnType::DATE => 'date', ColumnType::STRUCTURED => 'jsonb', ColumnType::JSON => 'jsonb', + ColumnType::ENUM => 'enum', default => 'varchar', }, 'timestamp without time zone' => 'timestamp', diff --git a/src/Column/ColumnFactory.php b/src/Column/ColumnFactory.php index 4664f1059..53f6d09bf 100644 --- a/src/Column/ColumnFactory.php +++ b/src/Column/ColumnFactory.php @@ -91,6 +91,7 @@ final class ColumnFactory extends AbstractColumnFactory 'xml' => ColumnType::STRING, 'json' => ColumnType::JSON, 'jsonb' => ColumnType::JSON, + 'enum' => ColumnType::ENUM, 'int4range' => PgsqlColumnType::INT4RANGE, 'int8range' => PgsqlColumnType::INT8RANGE, 'numrange' => PgsqlColumnType::NUMRANGE, From b3fa56d23fa7867e0e681c5feabdd4dbeba977db Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 24 Nov 2025 11:51:37 +0300 Subject: [PATCH 02/13] improve --- src/Column/ColumnDefinitionBuilder.php | 27 +++++++++++++++++++++++++- src/Column/ColumnFactory.php | 1 - src/DDLQueryBuilder.php | 3 +++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/Column/ColumnDefinitionBuilder.php b/src/Column/ColumnDefinitionBuilder.php index c75a1c33e..20f4322dc 100644 --- a/src/Column/ColumnDefinitionBuilder.php +++ b/src/Column/ColumnDefinitionBuilder.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Pgsql\Column; +use LogicException; use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\QueryBuilder\AbstractColumnDefinitionBuilder; @@ -11,6 +12,8 @@ use Yiisoft\Db\Schema\Column\CollatableColumnInterface; use Yiisoft\Db\Schema\Column\ColumnInterface; +use Yiisoft\Db\Schema\Column\EnumColumn; + use function str_repeat; use function version_compare; @@ -81,6 +84,28 @@ public function buildType(ColumnInterface $column): string return parent::buildType($column); } + protected function buildCheck(ColumnInterface $column): string + { + if ($column instanceof EnumColumn && $column->getDbType() === null) { + $check = $column->getCheck(); + $name = $column->getName(); + $items = $column->getEnumValues(); + if (empty($check) && !empty($name) && !empty($items)) { + $itemsList = implode( + ',', + array_map( + fn(string $item): string => $this->queryBuilder->getQuoter()->quoteValue($item), + $items + ), + ); + return " CHECK($name IN ($itemsList))"; + } + } + + return parent::buildCheck($column); + } + + protected function buildCollate(ColumnInterface $column): string { if (!$column instanceof CollatableColumnInterface || empty($column->getCollation())) { @@ -122,7 +147,7 @@ protected function getDbType(ColumnInterface $column): string ColumnType::DATE => 'date', ColumnType::STRUCTURED => 'jsonb', ColumnType::JSON => 'jsonb', - ColumnType::ENUM => 'enum', + ColumnType::ENUM => throw new LogicException('PostgreSQL does not support ENUM column type directly. Use a custom type instead.'), default => 'varchar', }, 'timestamp without time zone' => 'timestamp', diff --git a/src/Column/ColumnFactory.php b/src/Column/ColumnFactory.php index 53f6d09bf..4664f1059 100644 --- a/src/Column/ColumnFactory.php +++ b/src/Column/ColumnFactory.php @@ -91,7 +91,6 @@ final class ColumnFactory extends AbstractColumnFactory 'xml' => ColumnType::STRING, 'json' => ColumnType::JSON, 'jsonb' => ColumnType::JSON, - 'enum' => ColumnType::ENUM, 'int4range' => PgsqlColumnType::INT4RANGE, 'int8range' => PgsqlColumnType::INT8RANGE, 'numrange' => PgsqlColumnType::NUMRANGE, diff --git a/src/DDLQueryBuilder.php b/src/DDLQueryBuilder.php index e409ee444..4c9646685 100644 --- a/src/DDLQueryBuilder.php +++ b/src/DDLQueryBuilder.php @@ -6,9 +6,12 @@ use Throwable; use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\QueryBuilder\AbstractDDLQueryBuilder; use Yiisoft\Db\Schema\Column\ColumnInterface; +use Yiisoft\Db\Schema\Column\EnumColumn; + use function array_diff; use function explode; use function implode; From c006cc8bc665dfb9ab38f642c13b0570e55a7de8 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 24 Nov 2025 14:58:34 +0300 Subject: [PATCH 03/13] improve --- src/Column/ColumnDefinitionBuilder.php | 2 +- src/Schema.php | 29 ++++++++++++++++++++- tests/Column/EnumColumnTest.php | 36 ++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 tests/Column/EnumColumnTest.php diff --git a/src/Column/ColumnDefinitionBuilder.php b/src/Column/ColumnDefinitionBuilder.php index 20f4322dc..3d02aed85 100644 --- a/src/Column/ColumnDefinitionBuilder.php +++ b/src/Column/ColumnDefinitionBuilder.php @@ -147,7 +147,7 @@ protected function getDbType(ColumnInterface $column): string ColumnType::DATE => 'date', ColumnType::STRUCTURED => 'jsonb', ColumnType::JSON => 'jsonb', - ColumnType::ENUM => throw new LogicException('PostgreSQL does not support ENUM column type directly. Use a custom type instead.'), + ColumnType::ENUM => 'varchar', default => 'varchar', }, 'timestamp without time zone' => 'timestamp', diff --git a/src/Schema.php b/src/Schema.php index da557440f..0af94f0b5 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -527,7 +527,7 @@ private function loadColumn(array $info): ColumnInterface 'dbType' => $dbType, 'enumValues' => $info['enum_values'] !== null ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) - : null, + : $this->tryGetEnumValuesFromCheck($info['check']), 'name' => $info['column_name'], 'notNull' => !$info['is_nullable'], 'primaryKey' => $info['contype'] === 'p', @@ -679,4 +679,31 @@ private function loadTableConstraints(string $tableName, string $returnType): ar return $result[$returnType]; } + + /** + * @psalm-return list|null + */ + private function tryGetEnumValuesFromCheck(?string $check): ?array + { + if ($check === null) { + return null; + } + + preg_match_all( + "~ANY\s*\(\s*\(?\s*ARRAY\s*\[(?:[^'\]]+|'(?:''|[^'])*')*~i", + $check, + $block + ); + + if (empty($block[0][0])) { + return []; + } + + preg_match_all("~'((?:''|[^'])*)'~", $block[0][0], $matches); + + return array_map( + static fn($v) => str_replace("''", "'", $v), + $matches[1] ?? [], + ); + } } diff --git a/tests/Column/EnumColumnTest.php b/tests/Column/EnumColumnTest.php new file mode 100644 index 000000000..b74542d38 --- /dev/null +++ b/tests/Column/EnumColumnTest.php @@ -0,0 +1,36 @@ + Date: Mon, 24 Nov 2025 12:02:35 +0000 Subject: [PATCH 04/13] Apply PHP CS Fixer and Rector changes (CI) --- src/Column/ColumnDefinitionBuilder.php | 5 +---- src/DDLQueryBuilder.php | 3 --- src/Schema.php | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Column/ColumnDefinitionBuilder.php b/src/Column/ColumnDefinitionBuilder.php index 3d02aed85..f41763136 100644 --- a/src/Column/ColumnDefinitionBuilder.php +++ b/src/Column/ColumnDefinitionBuilder.php @@ -4,14 +4,12 @@ namespace Yiisoft\Db\Pgsql\Column; -use LogicException; use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\QueryBuilder\AbstractColumnDefinitionBuilder; use Yiisoft\Db\Schema\Column\AbstractArrayColumn; use Yiisoft\Db\Schema\Column\CollatableColumnInterface; use Yiisoft\Db\Schema\Column\ColumnInterface; - use Yiisoft\Db\Schema\Column\EnumColumn; use function str_repeat; @@ -95,7 +93,7 @@ protected function buildCheck(ColumnInterface $column): string ',', array_map( fn(string $item): string => $this->queryBuilder->getQuoter()->quoteValue($item), - $items + $items, ), ); return " CHECK($name IN ($itemsList))"; @@ -105,7 +103,6 @@ protected function buildCheck(ColumnInterface $column): string return parent::buildCheck($column); } - protected function buildCollate(ColumnInterface $column): string { if (!$column instanceof CollatableColumnInterface || empty($column->getCollation())) { diff --git a/src/DDLQueryBuilder.php b/src/DDLQueryBuilder.php index 4c9646685..e409ee444 100644 --- a/src/DDLQueryBuilder.php +++ b/src/DDLQueryBuilder.php @@ -6,12 +6,9 @@ use Throwable; use Yiisoft\Db\Exception\NotSupportedException; -use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\QueryBuilder\AbstractDDLQueryBuilder; use Yiisoft\Db\Schema\Column\ColumnInterface; -use Yiisoft\Db\Schema\Column\EnumColumn; - use function array_diff; use function explode; use function implode; diff --git a/src/Schema.php b/src/Schema.php index 0af94f0b5..b2e748c09 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -692,7 +692,7 @@ private function tryGetEnumValuesFromCheck(?string $check): ?array preg_match_all( "~ANY\s*\(\s*\(?\s*ARRAY\s*\[(?:[^'\]]+|'(?:''|[^'])*')*~i", $check, - $block + $block, ); if (empty($block[0][0])) { From 7e471dfd55ca44dbe534870756fdc9b40806329e Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 24 Nov 2025 15:45:14 +0300 Subject: [PATCH 05/13] fix --- src/Schema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Schema.php b/src/Schema.php index 0af94f0b5..f4f51367a 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -696,7 +696,7 @@ private function tryGetEnumValuesFromCheck(?string $check): ?array ); if (empty($block[0][0])) { - return []; + return null; } preg_match_all("~'((?:''|[^'])*)'~", $block[0][0], $matches); From 269321d6e9bbb153e769c7718cafa0e14b2c6db7 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 24 Nov 2025 16:13:57 +0300 Subject: [PATCH 06/13] improve --- src/Column/ColumnDefinitionBuilder.php | 2 +- src/Schema.php | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Column/ColumnDefinitionBuilder.php b/src/Column/ColumnDefinitionBuilder.php index f41763136..2cd034453 100644 --- a/src/Column/ColumnDefinitionBuilder.php +++ b/src/Column/ColumnDefinitionBuilder.php @@ -87,7 +87,7 @@ protected function buildCheck(ColumnInterface $column): string if ($column instanceof EnumColumn && $column->getDbType() === null) { $check = $column->getCheck(); $name = $column->getName(); - $items = $column->getEnumValues(); + $items = $column->getValues(); if (empty($check) && !empty($name) && !empty($items)) { $itemsList = implode( ',', diff --git a/src/Schema.php b/src/Schema.php index 1c8ab22db..e44c1b276 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -44,7 +44,7 @@ * column_default: string|null, * is_autoinc: bool|string, * sequence_name: string|null, - * enum_values: string|null, + * values: string|null, * size: int|string|null, * scale: int|string|null, * contype: string|null, @@ -292,7 +292,7 @@ protected function findColumns(TableSchemaInterface $table): bool )::varchar[], ',') ELSE NULL - END AS enum_values, + END AS values, COALESCE( information_schema._pg_char_max_length( COALESCE(td.oid, tb.oid, a.atttypid), @@ -467,7 +467,7 @@ protected function loadResultColumn(array $metadata): ?ColumnInterface )::varchar[], ',') ELSE NULL - END AS enum_values + END AS values FROM pg_type AS t LEFT JOIN pg_type AS t2 ON t.typcategory='A' AND t2.oid = t.typelem OR t.typbasetype > 0 AND t2.oid = t.typbasetype LEFT JOIN pg_namespace AS ns ON ns.oid = COALESCE(t2.typnamespace, t.typnamespace) @@ -486,8 +486,8 @@ protected function loadResultColumn(array $metadata): ?ColumnInterface } $columnInfo['type'] = ColumnType::STRUCTURED; - } elseif (!empty($typeInfo['enum_values'])) { - $columnInfo['enumValues'] = explode(',', str_replace(["''"], ["'"], $typeInfo['enum_values'])); + } elseif (!empty($typeInfo['values'])) { + $columnInfo['values'] = explode(',', str_replace(["''"], ["'"], $typeInfo['values'])); } } @@ -525,8 +525,8 @@ private function loadColumn(array $info): ColumnInterface 'collation' => $collation, 'comment' => $info['column_comment'], 'dbType' => $dbType, - 'enumValues' => $info['enum_values'] !== null - ? explode(',', str_replace(["''"], ["'"], $info['enum_values'])) + 'values' => $info['values'] !== null + ? explode(',', str_replace(["''"], ["'"], $info['values'])) : $this->tryGetEnumValuesFromCheck($info['check']), 'name' => $info['column_name'], 'notNull' => !$info['is_nullable'], From 72ce6ad4982998929fb6e0cd426d724b8a2dcc25 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 24 Nov 2025 17:03:20 +0300 Subject: [PATCH 07/13] improve --- src/Column/ColumnDefinitionBuilder.php | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/Column/ColumnDefinitionBuilder.php b/src/Column/ColumnDefinitionBuilder.php index 2cd034453..c1689f249 100644 --- a/src/Column/ColumnDefinitionBuilder.php +++ b/src/Column/ColumnDefinitionBuilder.php @@ -82,27 +82,6 @@ public function buildType(ColumnInterface $column): string return parent::buildType($column); } - protected function buildCheck(ColumnInterface $column): string - { - if ($column instanceof EnumColumn && $column->getDbType() === null) { - $check = $column->getCheck(); - $name = $column->getName(); - $items = $column->getValues(); - if (empty($check) && !empty($name) && !empty($items)) { - $itemsList = implode( - ',', - array_map( - fn(string $item): string => $this->queryBuilder->getQuoter()->quoteValue($item), - $items, - ), - ); - return " CHECK($name IN ($itemsList))"; - } - } - - return parent::buildCheck($column); - } - protected function buildCollate(ColumnInterface $column): string { if (!$column instanceof CollatableColumnInterface || empty($column->getCollation())) { From c26c329f4cbbe945fceb7fdc06ae1c4f8a471c2e Mon Sep 17 00:00:00 2001 From: vjik <525501+vjik@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:06:25 +0000 Subject: [PATCH 08/13] Apply PHP CS Fixer and Rector changes (CI) --- src/Column/ColumnDefinitionBuilder.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Column/ColumnDefinitionBuilder.php b/src/Column/ColumnDefinitionBuilder.php index c1689f249..1f38e8c60 100644 --- a/src/Column/ColumnDefinitionBuilder.php +++ b/src/Column/ColumnDefinitionBuilder.php @@ -10,7 +10,6 @@ use Yiisoft\Db\Schema\Column\AbstractArrayColumn; use Yiisoft\Db\Schema\Column\CollatableColumnInterface; use Yiisoft\Db\Schema\Column\ColumnInterface; -use Yiisoft\Db\Schema\Column\EnumColumn; use function str_repeat; use function version_compare; From f28fd16689774425ee04c509623bc1088b8f2b0f Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 25 Nov 2025 11:23:50 +0300 Subject: [PATCH 09/13] improve --- src/Schema.php | 2 +- tests/Column/EnumColumnTest.php | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Schema.php b/src/Schema.php index e44c1b276..27704c0cc 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -690,7 +690,7 @@ private function tryGetEnumValuesFromCheck(?string $check): ?array } preg_match_all( - "~ANY\s*\(\s*\(?\s*ARRAY\s*\[(?:[^'\]]+|'(?:''|[^'])*')*~i", + "~ANY\s*\(\s*\(?\s*ARRAY\s*\[(?:[^'\]]+|'(?:''|[^'])*')*\]\s*\)\s*::\s*(?:text|varchar|character varying)\s*\[~i", $check, $block, ); diff --git a/tests/Column/EnumColumnTest.php b/tests/Column/EnumColumnTest.php index b74542d38..1ca73fd39 100644 --- a/tests/Column/EnumColumnTest.php +++ b/tests/Column/EnumColumnTest.php @@ -4,13 +4,36 @@ namespace Yiisoft\Db\Pgsql\Tests\Column; +use PHPUnit\Framework\Attributes\TestWith; use Yiisoft\Db\Pgsql\Tests\Support\IntegrationTestTrait; +use Yiisoft\Db\Schema\Column\EnumColumn; use Yiisoft\Db\Tests\Common\CommonEnumColumnTest; final class EnumColumnTest extends CommonEnumColumnTest { use IntegrationTestTrait; + #[TestWith(['INTEGER CHECK (status IN (1, 2, 3))'])] + public function testNonEnumCheck(string $columnDefinition): void + { + $this->dropTable('test_enum_table'); + $this->executeStatements( + <<getSharedConnection(); + $column = $db->getTableSchema('test_enum_table')->getColumn('status'); + + $this->assertNotInstanceOf(EnumColumn::class, $column); + + $this->dropTable('test_enum_table'); + } + protected function createDatabaseObjectsStatements(): array { return [ From 8e0304cafc5585331d568b25d9aa631b690e9f43 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 25 Nov 2025 12:37:30 +0300 Subject: [PATCH 10/13] improve --- src/Schema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Schema.php b/src/Schema.php index 27704c0cc..96e678caa 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -690,7 +690,7 @@ private function tryGetEnumValuesFromCheck(?string $check): ?array } preg_match_all( - "~ANY\s*\(\s*\(?\s*ARRAY\s*\[(?:[^'\]]+|'(?:''|[^'])*')*\]\s*\)\s*::\s*(?:text|varchar|character varying)\s*\[~i", + "~ANY\s*\(\(ARRAY\[('(?:''|[^'])*'[^,\]]*)(?:,\s*(?1))*~", $check, $block, ); From 7389aa98ed15954bc950868a08426e223e9a6f6f Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 25 Nov 2025 13:18:44 +0300 Subject: [PATCH 11/13] test --- tests/Column/EnumColumnTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Column/EnumColumnTest.php b/tests/Column/EnumColumnTest.php index 1ca73fd39..ae6218e68 100644 --- a/tests/Column/EnumColumnTest.php +++ b/tests/Column/EnumColumnTest.php @@ -14,6 +14,7 @@ final class EnumColumnTest extends CommonEnumColumnTest use IntegrationTestTrait; #[TestWith(['INTEGER CHECK (status IN (1, 2, 3))'])] + #[TestWith(["TEXT CHECK (status != 'abc')"])] public function testNonEnumCheck(string $columnDefinition): void { $this->dropTable('test_enum_table'); From 8d0630c108a9dc576ea788d8218dee2c202caca8 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 29 Nov 2025 12:58:35 +0300 Subject: [PATCH 12/13] more tests --- tests/Column/EnumColumnTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Column/EnumColumnTest.php b/tests/Column/EnumColumnTest.php index ae6218e68..cabe38ac8 100644 --- a/tests/Column/EnumColumnTest.php +++ b/tests/Column/EnumColumnTest.php @@ -15,6 +15,8 @@ final class EnumColumnTest extends CommonEnumColumnTest #[TestWith(['INTEGER CHECK (status IN (1, 2, 3))'])] #[TestWith(["TEXT CHECK (status != 'abc')"])] + #[TestWith(["TEXT CHECK (status IN ('a', 'b') OR status = 'x')"])] + #[TestWith(["TEXT CHECK (status IN ('a', 'b') OR status IN ('x', 'y'))"])] public function testNonEnumCheck(string $columnDefinition): void { $this->dropTable('test_enum_table'); From 89af030b7b09dbbbe0c7b226e2b221ad1b0822c5 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 29 Nov 2025 13:25:18 +0300 Subject: [PATCH 13/13] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a0e4eac..32a9e6579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ - New #307: Add range and multirange columns support (@vjik, @Gerych1984) - Enh #464: Load column's check expressions for table schema (@Tigrov) - Bug #467: Fix column definition parsing in cases with parentheses (@vjik) +- New #465: Add enumeration column type support (@vjik) ## 1.3.0 March 21, 2024