Skip to content

Commit 84f1576

Browse files
authored
Merge pull request #2 from pfilsx/enum_remove_labels_support
Enum remove labels support
2 parents 9c81ccc + 22c98eb commit 84f1576

File tree

9 files changed

+264
-40
lines changed

9 files changed

+264
-40
lines changed

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
PostgreSQL Doctrine
22
==============
33

4+
[![Latest Stable Version](http://poser.pugx.org/pfilsx/postgresql-doctrine/v)](https://packagist.org/packages/pfilsx/postgresql-doctrine)
5+
[![PHP Version Require](http://poser.pugx.org/pfilsx/postgresql-doctrine/require/php)](https://packagist.org/packages/pfilsx/postgresql-doctrine)
6+
[![Total Downloads](http://poser.pugx.org/pfilsx/postgresql-doctrine/downloads)](https://packagist.org/packages/pfilsx/postgresql-doctrine)
7+
48
Description
59
------------
610

711
Provides extended Doctrine DBAL and Doctrine migration classes to allow you to use PostgreSQL
8-
specific features such as [enums](https://www.postgresql.org/docs/current/datatype-enum.html) with Doctrine.
12+
specific features such as [enums](https://www.postgresql.org/docs/current/datatype-enum.html) or JSON(B) with Doctrine.
913

1014
Features
1115
--------
@@ -36,4 +40,20 @@ Usage
3640

3741
Please refer [Doctrine DBAL](https://www.doctrine-project.org/projects/doctrine-dbal/en/current/index.html)
3842
and [Doctrine Migrations](https://www.doctrine-project.org/projects/doctrine-migrations/en/3.5/index.html)
39-
for instructions on how to override the default doctrine classes in your project.
43+
for instructions on how to override the default doctrine classes in your project.
44+
45+
Required steps:
46+
1. Register [PostgreSQLDriverMiddleware.php](src/DBAL/Middleware/PostgreSQLDriverMiddleware.php) as driver middleware
47+
2. Register [OrmSchemaProvider.php](src/Migrations/Provider/OrmSchemaProvider.php) as Doctrine\Migrations\Provider\SchemaProvider in Doctrine\Migrations\DependencyFactory
48+
49+
For Symfony integration see [PostgreSQLDoctrineBundle](https://github.com/pfilsx/PostgreSQLDoctrineBundle)
50+
51+
Documentation
52+
-------------
53+
54+
* [ENUMS](docs/ENUMS.md)
55+
56+
License
57+
-------
58+
59+
This bundle is released under the MIT license.

docs/ENUMS.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
Overview
2+
========
3+
4+
This package provides column type and extended DBAL classes for [PostgreSQL enums](https://www.postgresql.org/docs/current/datatype-enum.html) support for Doctrine and Doctrine migrations.
5+
Column type is based on enumType option of Column attribute/annotation.
6+
7+
Example
8+
--------
9+
10+
```php
11+
12+
enum ExampleEnum: string
13+
{
14+
case Case1 = 'case_1';
15+
case Case2 = 'case_2';
16+
}
17+
18+
#[Entity]
19+
class Example
20+
{
21+
/** ... */
22+
23+
#[Column(type: 'enum', enumType: ExampleEnum::class)]
24+
public ExampleEnum $type;
25+
}
26+
```
27+
28+
For the example above the doctrine migrations will generate migration with PostgreSQL enum type creation and column uses this type
29+
```php
30+
public function up(Schema $schema): void
31+
{
32+
$this->addSql('CREATE TYPE example_enum_type AS ENUM(\'case_1\', \'case_2\')');
33+
$this->addSql('COMMENT ON TYPE example_enum_type IS \'ExampleEnum\'');
34+
35+
$this->addSql('CREATE TABLE example (..., type example_enum_type NOT NULL, ...)');
36+
37+
/** ... */
38+
}
39+
40+
public function down(Schema $schema): void
41+
{
42+
/** ... */
43+
44+
$this->addSql('DROP TABLE example');
45+
$this->addSql('DROP TYPE example_enum_type');
46+
}
47+
```
48+
49+
50+
Supported actions in migrations
51+
-----------------
52+
53+
1. Create new enums based on usages in entities
54+
2. Remove enums based on usages in entities
55+
3. Update existing enum by adding new labels
56+
4. Recreate existing enum for removing labels
57+
58+
:warning: Attention :warning:
59+
60+
Adding/removing labels to existing enums will generate SQL which may fail on execution if your tables use those labels.
61+
Be careful and control your data!

src/DBAL/Platform/PostgreSQLPlatform.php

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,29 @@ public function createSchemaManager(Connection $connection): PostgreSQLSchemaMan
2323

2424
public function getListEnumTypesSQL(): string
2525
{
26-
return 'SELECT pg_type.typname AS name,
27-
pg_enum.enumlabel AS label,
28-
pg_description.description AS comment
29-
FROM pg_type
30-
JOIN pg_enum ON pg_enum.enumtypid = pg_type.oid
31-
LEFT JOIN pg_description on pg_description.objoid = pg_type.oid
32-
ORDER BY pg_enum.enumsortorder';
26+
return 'WITH types AS (
27+
SELECT pg_type.typname AS name,
28+
pg_enum.enumlabel AS label,
29+
pg_enum.enumsortorder AS label_order,
30+
pg_description.description AS comment,
31+
pg_class.relname AS usage_table,
32+
pg_attribute.attname AS usage_column,
33+
PG_GET_EXPR(pg_attrdef.adbin, pg_attrdef.adrelid) AS usage_default
34+
FROM pg_type
35+
JOIN pg_enum ON pg_enum.enumtypid = pg_type.oid
36+
LEFT JOIN pg_description ON pg_description.objoid = pg_type.oid
37+
LEFT JOIN pg_depend ON pg_depend.refobjid = pg_type.oid
38+
LEFT JOIN pg_class ON pg_class.oid = pg_depend.objid
39+
LEFT JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid AND pg_attribute.atttypid = pg_type.oid
40+
LEFT JOIN pg_attrdef ON pg_attrdef.adrelid = pg_class.oid AND pg_attrdef.adnum = pg_attribute.attnum
41+
)
42+
SELECT types.name,
43+
types.comment,
44+
JSON_AGG(DISTINCT JSONB_BUILD_OBJECT(\'label\', types.label, \'order\', types.label_order)) AS labels,
45+
JSON_AGG(DISTINCT JSONB_BUILD_OBJECT(\'table\', types.usage_table, \'column\', types.usage_column, \'default\', types.usage_default)) FILTER (WHERE types.usage_table IS NOT NULL AND types.usage_column IS NOT NULL) AS usages
46+
FROM types
47+
GROUP BY types.name, types.comment'
48+
;
3349
}
3450

3551
/**
@@ -59,13 +75,48 @@ public function getAlterTypeSql(EnumTypeAsset $from, EnumTypeAsset $to): array
5975
$toLabels = $to->getLabels();
6076

6177
$result = [];
78+
$typeName = $to->getQuotedName($this);
79+
6280
foreach (array_diff($toLabels, $fromLabels) as $label) {
63-
$result[] = "ALTER TYPE {$to->getQuotedName($this)} ADD VALUE {$this->quoteEnumLabel($label)}";
81+
$result[] = "ALTER TYPE {$typeName} ADD VALUE {$this->quoteEnumLabel($label)}";
6482
}
6583

66-
if (count(array_diff($fromLabels, $toLabels)) > 0) {
67-
throw new Exception('Enum labels reduction is not supported in automatic generation');
84+
$removedLabels = array_diff($fromLabels, $toLabels);
85+
86+
if (count($removedLabels) < 1) {
87+
return $result;
6888
}
89+
90+
$self = $this;
91+
92+
$result[] = "ALTER TYPE {$typeName} RENAME TO {$typeName}_old";
93+
$result[] = $this->getCreateTypeSql($to);
94+
$result[] = $this->getCommentOnTypeSql($to);
95+
96+
foreach ($to->getUsages() as $usage) {
97+
$tableName = $this->quoteIdentifier($usage->getTable());
98+
$columnName = $this->quoteIdentifier($usage->getColumn());
99+
if (($default = $usage->getDefault()) !== null) {
100+
$result[] = sprintf('ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT', $tableName, $columnName);
101+
}
102+
$result[] = sprintf(
103+
'ALTER TABLE %1$s ALTER COLUMN %2$s TYPE %3$s USING LOWER(%2$s::text)::%3$s',
104+
$tableName,
105+
$columnName,
106+
$typeName
107+
);
108+
109+
if ($default !== null) {
110+
$result[] = sprintf(
111+
'ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s',
112+
$tableName,
113+
$columnName,
114+
$this->quoteEnumLabel($default)
115+
);
116+
}
117+
}
118+
119+
$result[] = "DROP TYPE {$typeName}_old";
69120

70121
return $result;
71122
}

src/DBAL/Schema/EnumTypeAsset.php

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,23 @@
1414
final class EnumTypeAsset extends AbstractAsset
1515
{
1616
/**
17-
* @var array<int|string>
17+
* @var string[]
1818
*/
1919
private array $labels;
2020
private string $enumClass;
21+
/**
22+
* @var EnumTypeUsageAsset[]
23+
*/
24+
private array $usages;
2125

2226
/**
2327
* @param string $name
2428
* @param string $className
25-
* @param array<int|string> $labels
29+
* @param string[] $labels
30+
* @param EnumTypeUsageAsset[] $usages
2631
* @throws InvalidArgumentException
2732
*/
28-
public function __construct(string $name, string $className, array $labels = [])
33+
public function __construct(string $name, string $className, array $labels = [], array $usages = [])
2934
{
3035
if ($name === '') {
3136
throw new InvalidArgumentException('Invalid custom type name specified');
@@ -34,24 +39,25 @@ public function __construct(string $name, string $className, array $labels = [])
3439
$this->_setName($name);
3540
$this->enumClass = $className;
3641
$this->labels = $labels;
42+
$this->usages = $usages;
3743
}
3844

3945
/**
4046
* @param class-string<EnumInterface|\UnitEnum> $className
4147
* @throws InvalidArgumentException
4248
* @return static
4349
*/
44-
public static function fromEnumClassName(string $className): self
50+
public static function fromEnumClassName(string $name, string $className): self
4551
{
4652
return new self(
47-
EnumTool::getEnumTypeNameFromClassName($className),
53+
$name,
4854
$className,
4955
EnumTool::getEnumLabelsByClassName($className)
5056
);
5157
}
5258

5359
/**
54-
* @return array<int|string>
60+
* @return string[]
5561
*/
5662
public function getLabels(): array
5763
{
@@ -63,6 +69,21 @@ public function getEnumClass(): string
6369
return $this->enumClass;
6470
}
6571

72+
public function addUsage(EnumTypeUsageAsset $usage): self
73+
{
74+
$this->usages[] = $usage;
75+
76+
return $this;
77+
}
78+
79+
/**
80+
* @return EnumTypeUsageAsset[]
81+
*/
82+
public function getUsages(): array
83+
{
84+
return $this->usages;
85+
}
86+
6687
/**
6788
* @param AbstractPlatform $platform
6889
* @throws InvalidArgumentException
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Pfilsx\PostgreSQLDoctrine\DBAL\Schema;
5+
6+
final class EnumTypeUsageAsset
7+
{
8+
private string $table;
9+
10+
private string $column;
11+
12+
private ?string $default;
13+
14+
public function __construct(string $table, string $column, ?string $default = null)
15+
{
16+
$this->table = $table;
17+
$this->column = $column;
18+
$this->default = $default;
19+
}
20+
21+
public function getTable(): string
22+
{
23+
return $this->table;
24+
}
25+
26+
public function getColumn(): string
27+
{
28+
return $this->column;
29+
}
30+
31+
public function getDefault(): ?string
32+
{
33+
return $this->default;
34+
}
35+
}

src/DBAL/Schema/PostgreSQLSchemaManager.php

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -330,17 +330,29 @@ protected function _getPortableTableColumnDefinition($tableColumn): Column
330330

331331
private function getPortableEnumTypesList(array $rawTypes): array
332332
{
333-
$typeGroups = [];
334-
foreach ($rawTypes as $rawType) {
335-
$typeGroup = $typeGroups[$rawType['name']] ?? [];
336-
$typeGroup['comment'] = $rawType['comment'];
337-
$typeGroup['labels'][] = $rawType['label'];
338-
$typeGroups[$rawType['name']] = $typeGroup;
339-
}
340-
341333
$list = [];
342-
foreach ($typeGroups as $name => $typeGroup) {
343-
$list[] = new EnumTypeAsset($name, $typeGroup['comment'], $typeGroup['labels']);
334+
foreach ($rawTypes as $rawType) {
335+
$labels = json_decode($rawType['labels'], true);
336+
usort($labels, static fn (array $a, array $b) => $a['order'] <=> $b['order']);
337+
338+
$usages = json_decode($rawType['usages'], true);
339+
foreach ($usages as &$usage) {
340+
$default = $usage['default'] ?? null;
341+
if ($default !== null) {
342+
$default = trim(explode('::', $default)[0], '\'');
343+
}
344+
$usage['default'] = $default;
345+
}
346+
347+
$list[] = new EnumTypeAsset(
348+
$rawType['name'],
349+
$rawType['comment'],
350+
array_column($labels, 'label'),
351+
array_map(
352+
static fn (array $usage) => new EnumTypeUsageAsset($usage['table'], $usage['column'], $usage['default']),
353+
$usages
354+
)
355+
);
344356
}
345357

346358
return $list;

0 commit comments

Comments
 (0)