From 9ef3b5741bb7c2062fa64496f45e02df447399e5 Mon Sep 17 00:00:00 2001 From: ErfanMomeniii Date: Thu, 14 May 2026 23:38:31 +0330 Subject: [PATCH 1/3] feat(postgres): add native enum type support --- src/Phinx/Db/Adapter/AdapterInterface.php | 6 +- src/Phinx/Db/Adapter/PostgresAdapter.php | 147 +++++++++++++++- src/Phinx/Db/Table/Column.php | 4 +- .../Phinx/Db/Adapter/PostgresAdapterTest.php | 166 ++++++++++++++++++ 4 files changed, 312 insertions(+), 11 deletions(-) diff --git a/src/Phinx/Db/Adapter/AdapterInterface.php b/src/Phinx/Db/Adapter/AdapterInterface.php index 12cf21d26..60107920b 100644 --- a/src/Phinx/Db/Adapter/AdapterInterface.php +++ b/src/Phinx/Db/Adapter/AdapterInterface.php @@ -70,12 +70,14 @@ interface AdapterInterface self::PHINX_TYPE_POLYGON, ]; - // only for mysql so far + // MySQL-specific types public const PHINX_TYPE_MEDIUM_INTEGER = 'mediuminteger'; - public const PHINX_TYPE_ENUM = 'enum'; public const PHINX_TYPE_SET = 'set'; public const PHINX_TYPE_YEAR = 'year'; + // Supported by MySQL and PostgreSQL + public const PHINX_TYPE_ENUM = 'enum'; + // only for postgresql so far public const PHINX_TYPE_CIDR = 'cidr'; public const PHINX_TYPE_INET = 'inet'; diff --git a/src/Phinx/Db/Adapter/PostgresAdapter.php b/src/Phinx/Db/Adapter/PostgresAdapter.php index 9dc0fe431..4c5322221 100644 --- a/src/Phinx/Db/Adapter/PostgresAdapter.php +++ b/src/Phinx/Db/Adapter/PostgresAdapter.php @@ -42,6 +42,7 @@ class PostgresAdapter extends PdoAdapter self::PHINX_TYPE_MACADDR, self::PHINX_TYPE_INTERVAL, self::PHINX_TYPE_BINARYUUID, + self::PHINX_TYPE_ENUM, ]; private const GIN_INDEX_TYPE = 'gin'; @@ -277,7 +278,7 @@ public function createTable(Table $table, array $columns = [], array $indexes = $this->columnsWithComments = []; foreach ($columns as $column) { - $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column); + $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column, $table->getName()); if ($this->useIdentity && $column->getIdentity() && $column->getGenerated() !== null) { $sql .= sprintf(' GENERATED %s AS IDENTITY', $column->getGenerated()); } @@ -304,6 +305,22 @@ public function createTable(Table $table, array $columns = [], array $indexes = } $sql .= ')'; + + // Emit CREATE TYPE ... AS ENUM statements before the CREATE TABLE statement + foreach ($columns as $column) { + if ($column->getType() === static::PHINX_TYPE_ENUM) { + $values = $column->getValues(); + if (!empty($values)) { + $typeName = $this->getEnumTypeName($table->getName(), $column->getName()); + $queries[] = sprintf( + 'CREATE TYPE %s AS ENUM (%s)', + $this->quoteColumnName($typeName), + implode(', ', array_map(fn($v) => $this->getConnection()->quote($v), $values)), + ); + } + } + } + $queries[] = $sql; // process column comments @@ -423,8 +440,20 @@ protected function getDropTableInstructions(string $tableName): AlterInstruction { $this->removeCreatedTable($tableName); $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName)); + $instructions = new AlterInstructions([], [$sql]); + + // Drop any Phinx-managed enum types associated with this table's columns + foreach ($this->getColumns($tableName) as $column) { + $typeName = $this->getEnumTypeName($tableName, $column->getName()); + if ($this->hasEnumType($typeName)) { + $instructions->addPostStep(sprintf( + 'DROP TYPE %s', + $this->quoteColumnName($typeName), + )); + } + } - return new AlterInstructions([], [$sql]); + return $instructions; } /** @@ -462,9 +491,15 @@ public function getColumns(string $tableName): array $columnsInfo = $this->fetchAll($sql); foreach ($columnsInfo as $columnInfo) { $isUserDefined = strtoupper(trim($columnInfo['data_type'])) === 'USER-DEFINED'; + $enumValues = null; if ($isUserDefined) { - $columnType = Literal::from($columnInfo['udt_name']); + $enumValues = $this->getEnumTypeValues($columnInfo['udt_name']); + if ($enumValues !== null) { + $columnType = static::PHINX_TYPE_ENUM; + } else { + $columnType = Literal::from($columnInfo['udt_name']); + } } else { $columnType = $this->getPhinxType($columnInfo['data_type']); } @@ -512,6 +547,11 @@ public function getColumns(string $tableName): array } elseif ($columnType === self::PHINX_TYPE_DECIMAL) { $column->setPrecision($columnInfo['numeric_precision']); } + + if ($enumValues !== null) { + $column->setValues($enumValues); + } + $columns[] = $column; } @@ -543,11 +583,23 @@ public function hasColumn(string $tableName, string $columnName): bool */ protected function getAddColumnInstructions(Table $table, Column $column): AlterInstructions { + if ($column->getType() === static::PHINX_TYPE_ENUM) { + $values = $column->getValues(); + if (!empty($values)) { + $typeName = $this->getEnumTypeName($table->getName(), $column->getName()); + $this->execute(sprintf( + 'CREATE TYPE %s AS ENUM (%s)', + $this->quoteColumnName($typeName), + implode(', ', array_map(fn($v) => $this->getConnection()->quote($v), $values)), + )); + } + } + $instructions = new AlterInstructions(); $instructions->addAlter(sprintf( 'ADD %s %s %s', $this->quoteColumnName($column->getName()), - $this->getColumnSqlDefinition($column), + $this->getColumnSqlDefinition($column, $table->getName()), $column->isIdentity() && $column->getGenerated() !== null && $this->useIdentity ? sprintf('GENERATED %s AS IDENTITY', $column->getGenerated()) : '', )); @@ -614,7 +666,7 @@ protected function getChangeColumnInstructions( $sql = sprintf( 'ALTER COLUMN %s TYPE %s', $quotedColumnName, - $this->getColumnSqlDefinition($newColumn), + $this->getColumnSqlDefinition($newColumn, $tableName), ); if (in_array($newColumn->getType(), ['smallinteger', 'integer', 'biginteger'], true)) { $sql .= sprintf( @@ -735,7 +787,18 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa $this->quoteColumnName($columnName), ); - return new AlterInstructions([$alter]); + $instructions = new AlterInstructions([$alter]); + + // Drop the associated Phinx-managed enum type if it exists + $typeName = $this->getEnumTypeName($tableName, $columnName); + if ($this->hasEnumType($typeName)) { + $instructions->addPostStep(sprintf( + 'DROP TYPE %s', + $this->quoteColumnName($typeName), + )); + } + + return $instructions; } /** @@ -1098,6 +1161,8 @@ public function getSqlType(Literal|string $type, ?int $limit = null): array return ['name' => 'bytea']; case static::PHINX_TYPE_INTERVAL: return ['name' => 'interval']; + case static::PHINX_TYPE_ENUM: + return ['name' => 'enum']; // Geospatial database types // Spatial storage in Postgres is done via the PostGIS extension, // which enables the use of the "geography" type in combination @@ -1227,9 +1292,10 @@ public function dropDatabase($name): void * Gets the PostgreSQL Column Definition for a Column object. * * @param \Phinx\Db\Table\Column $column Column + * @param string|null $tableName Table name, used to derive the enum type name when column type is 'enum' * @return string */ - protected function getColumnSqlDefinition(Column $column): string + protected function getColumnSqlDefinition(Column $column, ?string $tableName = null): string { $buffer = []; @@ -1241,6 +1307,11 @@ protected function getColumnSqlDefinition(Column $column): string } else { $buffer[] = 'SERIAL'; } + } elseif ($column->getType() === static::PHINX_TYPE_ENUM) { + $typeName = $tableName !== null + ? $this->getEnumTypeName($tableName, $column->getName()) + : $column->getName(); + $buffer[] = $this->quoteColumnName($typeName); } elseif ($column->getType() instanceof Literal) { $buffer[] = (string)$column->getType(); } else { @@ -1295,6 +1366,68 @@ protected function getColumnSqlDefinition(Column $column): string return implode(' ', $buffer); } + /** + * Returns the Phinx-managed PostgreSQL enum type name for a given table column. + * + * The convention is `{table}_{column}` using the unqualified table name so that + * two tables cannot accidentally share the same type with different values. + * + * @param string $tableName Table name (may be schema-qualified) + * @param string $columnName Column name + * @return string + */ + protected function getEnumTypeName(string $tableName, string $columnName): string + { + return $this->getSchemaName($tableName)['table'] . '_' . $columnName; + } + + /** + * Returns the enum label values for a given PostgreSQL enum type name, or null if the + * type does not exist or is not an enum. + * + * @param string $typeName Type name (unqualified) + * @return string[]|null + */ + protected function getEnumTypeValues(string $typeName): ?array + { + $sql = sprintf( + "SELECT e.enumlabel + FROM pg_type t + JOIN pg_enum e ON t.oid = e.enumtypid + JOIN pg_namespace n ON t.typnamespace = n.oid + WHERE t.typname = %s AND t.typtype = 'e' AND n.nspname = %s + ORDER BY e.enumsortorder", + $this->getConnection()->quote($typeName), + $this->getConnection()->quote($this->schema), + ); + $rows = $this->fetchAll($sql); + + return !empty($rows) ? array_column($rows, 'enumlabel') : null; + } + + /** + * Returns true if a PostgreSQL enum type with the given unqualified name exists in the + * current schema. + * + * @param string $typeName Type name (unqualified) + * @return bool + */ + protected function hasEnumType(string $typeName): bool + { + $sql = sprintf( + "SELECT EXISTS( + SELECT 1 FROM pg_type t + JOIN pg_namespace n ON t.typnamespace = n.oid + WHERE t.typname = %s AND t.typtype = 'e' AND n.nspname = %s + ) AS type_exists", + $this->getConnection()->quote($typeName), + $this->getConnection()->quote($this->schema), + ); + $result = $this->fetchRow($sql); + + return (bool)$result['type_exists']; + } + /** * Gets the PostgreSQL Column Comment Definition for a column object. * diff --git a/src/Phinx/Db/Table/Column.php b/src/Phinx/Db/Table/Column.php index 9ce50de3c..4131d04a2 100644 --- a/src/Phinx/Db/Table/Column.php +++ b/src/Phinx/Db/Table/Column.php @@ -40,14 +40,14 @@ class Column /** MySQL-only column type */ public const MEDIUMINTEGER = AdapterInterface::PHINX_TYPE_MEDIUM_INTEGER; /** MySQL-only column type */ - public const ENUM = AdapterInterface::PHINX_TYPE_ENUM; - /** MySQL-only column type */ public const SET = AdapterInterface::PHINX_TYPE_STRING; /** MySQL-only column type */ public const BLOB = AdapterInterface::PHINX_TYPE_BLOB; /** MySQL-only column type */ public const YEAR = AdapterInterface::PHINX_TYPE_YEAR; /** MySQL/Postgres-only column type */ + public const ENUM = AdapterInterface::PHINX_TYPE_ENUM; + /** MySQL/Postgres-only column type */ public const JSON = AdapterInterface::PHINX_TYPE_JSON; /** Postgres-only column type */ public const JSONB = AdapterInterface::PHINX_TYPE_JSONB; diff --git a/tests/Phinx/Db/Adapter/PostgresAdapterTest.php b/tests/Phinx/Db/Adapter/PostgresAdapterTest.php index d1db27e39..9fe8e4f7b 100644 --- a/tests/Phinx/Db/Adapter/PostgresAdapterTest.php +++ b/tests/Phinx/Db/Adapter/PostgresAdapterTest.php @@ -3048,4 +3048,170 @@ public function testPdoNotPersistentConnection() $adapter = new PostgresAdapter(PGSQL_DB_CONFIG); $this->assertFalse($adapter->getConnection()->getAttribute(PDO::ATTR_PERSISTENT)); } + + public function testGetSqlTypeEnum() + { + $this->assertEquals(['name' => 'enum'], $this->adapter->getSqlType('enum')); + } + + public function testEnumIsValidColumnType() + { + $col = new Column(); + $col->setType('enum'); + $this->assertTrue($this->adapter->isValidColumnType($col)); + } + + public function testCreateTableWithEnumColumn() + { + $table = new Table('moods', ['id' => false], $this->adapter); + $table->addColumn('current_mood', 'enum', [ + 'values' => ['sad', 'ok', 'happy'], + 'null' => false, + ])->save(); + + $this->assertTrue($this->adapter->hasTable('moods')); + $this->assertTrue($this->adapter->hasColumn('moods', 'current_mood')); + + // The PostgreSQL type should exist + $result = $this->adapter->fetchRow( + "SELECT EXISTS(SELECT 1 FROM pg_type WHERE typname = 'moods_current_mood' AND typtype = 'e') AS type_exists", + ); + $this->assertTrue((bool)$result['type_exists'], 'Enum type moods_current_mood should exist'); + } + + public function testCreateTableWithEnumColumnRoundTrip() + { + $values = ['pending', 'active', 'archived']; + + $table = new Table('orders', ['id' => false], $this->adapter); + $table->addColumn('status', 'enum', ['values' => $values, 'null' => false])->save(); + + $columns = $this->adapter->getColumns('orders'); + $statusColumn = null; + foreach ($columns as $column) { + if ($column->getName() === 'status') { + $statusColumn = $column; + break; + } + } + + $this->assertNotNull($statusColumn, 'Column status should exist'); + $this->assertEquals('enum', $statusColumn->getType()); + $this->assertEquals($values, $statusColumn->getValues()); + } + + public function testAddEnumColumn() + { + $table = new Table('table1', [], $this->adapter); + $table->save(); + + $table->addColumn('mood', 'enum', [ + 'values' => ['happy', 'sad', 'neutral'], + 'null' => true, + ])->save(); + + $this->assertTrue($this->adapter->hasColumn('table1', 'mood')); + + // The type should have been created + $result = $this->adapter->fetchRow( + "SELECT EXISTS(SELECT 1 FROM pg_type WHERE typname = 'table1_mood' AND typtype = 'e') AS type_exists", + ); + $this->assertTrue((bool)$result['type_exists'], 'Enum type table1_mood should exist'); + } + + public function testAddEnumColumnRoundTrip() + { + $values = ['draft', 'published', 'deleted']; + + $table = new Table('articles', [], $this->adapter); + $table->save(); + $table->addColumn('state', 'enum', ['values' => $values, 'null' => false])->save(); + + $columns = $this->adapter->getColumns('articles'); + $stateColumn = null; + foreach ($columns as $column) { + if ($column->getName() === 'state') { + $stateColumn = $column; + break; + } + } + + $this->assertNotNull($stateColumn, 'Column state should exist'); + $this->assertEquals('enum', $stateColumn->getType()); + $this->assertEquals($values, $stateColumn->getValues()); + } + + public function testDropColumnDropsEnumType() + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('status', 'enum', ['values' => ['on', 'off']])->save(); + + $this->assertTrue($this->adapter->hasColumn('table1', 'status')); + + $result = $this->adapter->fetchRow( + "SELECT EXISTS(SELECT 1 FROM pg_type WHERE typname = 'table1_status' AND typtype = 'e') AS type_exists", + ); + $this->assertTrue((bool)$result['type_exists'], 'Enum type should exist before drop'); + + $table->removeColumn('status')->save(); + + $this->assertFalse($this->adapter->hasColumn('table1', 'status')); + + $result = $this->adapter->fetchRow( + "SELECT EXISTS(SELECT 1 FROM pg_type WHERE typname = 'table1_status' AND typtype = 'e') AS type_exists", + ); + $this->assertFalse((bool)$result['type_exists'], 'Enum type should be dropped along with the column'); + } + + public function testDropTableDropsEnumTypes() + { + $table = new Table('notifications', ['id' => false], $this->adapter); + $table->addColumn('kind', 'enum', ['values' => ['email', 'sms', 'push'], 'null' => false]) + ->addColumn('state', 'enum', ['values' => ['queued', 'sent', 'failed'], 'null' => false]) + ->save(); + + $this->assertTrue($this->adapter->hasTable('notifications')); + + $table->drop()->save(); + + $this->assertFalse($this->adapter->hasTable('notifications')); + + foreach (['notifications_kind', 'notifications_state'] as $typeName) { + $result = $this->adapter->fetchRow(sprintf( + "SELECT EXISTS(SELECT 1 FROM pg_type WHERE typname = '%s' AND typtype = 'e') AS type_exists", + $typeName, + )); + $this->assertFalse((bool)$result['type_exists'], "Enum type $typeName should be dropped with the table"); + } + } + + public function testTwoTablesCanHaveEnumColumnsWithSameName() + { + $table1 = new Table('users', ['id' => false], $this->adapter); + $table1->addColumn('status', 'enum', ['values' => ['active', 'inactive']])->save(); + + $table2 = new Table('orders', ['id' => false], $this->adapter); + $table2->addColumn('status', 'enum', ['values' => ['pending', 'shipped', 'delivered']])->save(); + + // Each table gets its own enum type + foreach (['users_status' => ['active', 'inactive'], 'orders_status' => ['pending', 'shipped', 'delivered']] as $typeName => $expectedValues) { + $rows = $this->adapter->fetchAll(sprintf( + "SELECT e.enumlabel FROM pg_type t JOIN pg_enum e ON t.oid = e.enumtypid + WHERE t.typname = '%s' ORDER BY e.enumsortorder", + $typeName, + )); + $this->assertEquals($expectedValues, array_column($rows, 'enumlabel'), "Values for $typeName should match"); + } + } + + public function testCreateTableWithEnumColumnWithSchema() + { + $this->adapter->createSchema('tschema'); + + $table = new Table('tschema.events', ['id' => false], $this->adapter); + $table->addColumn('level', 'enum', ['values' => ['info', 'warning', 'error'], 'null' => false])->save(); + + $this->assertTrue($this->adapter->hasTable('tschema.events')); + $this->assertTrue($this->adapter->hasColumn('tschema.events', 'level')); + } } From 38c1b226f3d11377d937464efb217be1721f88dd Mon Sep 17 00:00:00 2001 From: ErfanMomeniii Date: Fri, 15 May 2026 00:15:45 +0330 Subject: [PATCH 2/3] test: add some more test to increase coverage and support edge case for postgres enum --- .../Phinx/Db/Adapter/PostgresAdapterTest.php | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/tests/Phinx/Db/Adapter/PostgresAdapterTest.php b/tests/Phinx/Db/Adapter/PostgresAdapterTest.php index 9fe8e4f7b..82496bc0d 100644 --- a/tests/Phinx/Db/Adapter/PostgresAdapterTest.php +++ b/tests/Phinx/Db/Adapter/PostgresAdapterTest.php @@ -3214,4 +3214,211 @@ public function testCreateTableWithEnumColumnWithSchema() $this->assertTrue($this->adapter->hasTable('tschema.events')); $this->assertTrue($this->adapter->hasColumn('tschema.events', 'level')); } + + public function testCreateTableWithEnumColumnAndDefault() + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('status', 'enum', [ + 'values' => ['pending', 'active', 'archived'], + 'null' => false, + 'default' => 'pending', + ])->save(); + + $this->assertTrue($this->adapter->hasColumn('table1', 'status')); + + $columns = $this->adapter->getColumns('table1'); + foreach ($columns as $column) { + if ($column->getName() === 'status') { + $this->assertEquals('enum', $column->getType()); + $this->assertEquals('pending', $column->getDefault()); + $this->assertFalse($column->isNull()); + } + } + } + + public function testNullableEnumColumnRoundTrip() + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('priority', 'enum', [ + 'values' => ['low', 'medium', 'high'], + 'null' => true, + ])->save(); + + $columns = $this->adapter->getColumns('table1'); + foreach ($columns as $column) { + if ($column->getName() === 'priority') { + $this->assertEquals('enum', $column->getType()); + $this->assertEquals(['low', 'medium', 'high'], $column->getValues()); + $this->assertTrue($column->isNull()); + } + } + } + + public function testDropNonEnumColumnKeepsEnumType() + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('status', 'enum', ['values' => ['on', 'off']]) + ->addColumn('name', 'string') + ->save(); + + $this->assertTrue($this->adapter->hasColumn('table1', 'status')); + $this->assertTrue($this->adapter->hasColumn('table1', 'name')); + + // Drop the non-enum column + $table->removeColumn('name')->save(); + + $this->assertFalse($this->adapter->hasColumn('table1', 'name')); + $this->assertTrue($this->adapter->hasColumn('table1', 'status')); + + // The enum type should still exist + $result = $this->adapter->fetchRow( + "SELECT EXISTS(SELECT 1 FROM pg_type WHERE typname = 'table1_status' AND typtype = 'e') AS type_exists", + ); + $this->assertTrue((bool)$result['type_exists'], 'Enum type should not be dropped when removing a non-enum column'); + } + + public function testDropTableWithMixedEnumAndNonEnumColumns() + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('status', 'enum', ['values' => ['on', 'off']]) + ->addColumn('name', 'string') + ->addColumn('age', 'integer') + ->save(); + + $table->drop()->save(); + + $this->assertFalse($this->adapter->hasTable('table1')); + + // Enum type should be cleaned up + $result = $this->adapter->fetchRow( + "SELECT EXISTS(SELECT 1 FROM pg_type WHERE typname = 'table1_status' AND typtype = 'e') AS type_exists", + ); + $this->assertFalse((bool)$result['type_exists'], 'Enum type should be dropped with the table'); + + // Non-enum columns should not leave orphaned types + $result = $this->adapter->fetchRow( + "SELECT EXISTS(SELECT 1 FROM pg_type WHERE typname = 'table1_name' AND typtype = 'e') AS type_exists", + ); + $this->assertFalse((bool)$result['type_exists'], 'No orphan type for non-enum column'); + } + + public function testGetColumnsWithEnumAndLiteralUserDefinedType() + { + // Create a table with both an enum column and a citext (USER-DEFINED, non-enum) column + $table = new Table('table1', ['id' => false], $this->adapter); + $table->addColumn('mood', 'enum', ['values' => ['happy', 'sad']]) + ->addColumn('label', Literal::from('citext')) + ->save(); + + $columns = $this->adapter->getColumns('table1'); + $this->assertCount(2, $columns); + + $moodColumn = null; + $labelColumn = null; + foreach ($columns as $column) { + if ($column->getName() === 'mood') { + $moodColumn = $column; + } + if ($column->getName() === 'label') { + $labelColumn = $column; + } + } + + // Enum column should come back as PHINX_TYPE_ENUM with values + $this->assertNotNull($moodColumn); + $this->assertEquals('enum', $moodColumn->getType()); + $this->assertEquals(['happy', 'sad'], $moodColumn->getValues()); + + // citext column should remain a Literal (USER-DEFINED non-enum) + $this->assertNotNull($labelColumn); + $this->assertInstanceOf(Literal::class, $labelColumn->getType()); + $this->assertEquals('citext', (string)$labelColumn->getType()); + } + + public function testAddEnumColumnWithDefaultValue() + { + $table = new Table('table1', [], $this->adapter); + $table->save(); + + $table->addColumn('role', 'enum', [ + 'values' => ['admin', 'editor', 'viewer'], + 'null' => false, + 'default' => 'viewer', + ])->save(); + + $this->assertTrue($this->adapter->hasColumn('table1', 'role')); + + $columns = $this->adapter->getColumns('table1'); + foreach ($columns as $column) { + if ($column->getName() === 'role') { + $this->assertEquals('enum', $column->getType()); + $this->assertEquals(['admin', 'editor', 'viewer'], $column->getValues()); + $this->assertEquals('viewer', $column->getDefault()); + $this->assertFalse($column->isNull()); + } + } + } + + public function testChangeColumnPreservesEnumOnOtherColumns() + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('status', 'enum', ['values' => ['active', 'inactive']]) + ->addColumn('name', 'string', ['limit' => 50]) + ->save(); + + // Change the non-enum column + $newColumn = new Column(); + $newColumn->setName('name') + ->setType('string') + ->setLimit(100); + + $table->changeColumn('name', $newColumn)->save(); + + // Verify the enum column is intact + $columns = $this->adapter->getColumns('table1'); + foreach ($columns as $column) { + if ($column->getName() === 'status') { + $this->assertEquals('enum', $column->getType()); + $this->assertEquals(['active', 'inactive'], $column->getValues()); + } + if ($column->getName() === 'name') { + $this->assertEquals('100', $column->getLimit()); + } + } + } + + public function testEnumValuesOrderIsPreserved() + { + $values = ['zebra', 'apple', 'mango', 'banana']; + + $table = new Table('table1', ['id' => false], $this->adapter); + $table->addColumn('fruit', 'enum', ['values' => $values])->save(); + + $columns = $this->adapter->getColumns('table1'); + foreach ($columns as $column) { + if ($column->getName() === 'fruit') { + $this->assertSame($values, $column->getValues(), 'Enum values should preserve insertion order'); + } + } + } + + public function testInsertDataWithEnumColumn() + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('status', 'enum', [ + 'values' => ['pending', 'active', 'closed'], + 'null' => false, + 'default' => 'pending', + ])->save(); + + $table->insert([ + ['status' => 'active'], + ['status' => 'closed'], + ])->save(); + + $rows = $this->adapter->fetchAll('SELECT status FROM table1 ORDER BY id'); + $this->assertCount(2, $rows); + $this->assertEquals('active', $rows[0]['status']); + $this->assertEquals('closed', $rows[1]['status']); + } } From 8986ba69dab71961ab6abe08b0eb58cfe77d76b9 Mon Sep 17 00:00:00 2001 From: ErfanMomeniii Date: Fri, 15 May 2026 00:26:20 +0330 Subject: [PATCH 3/3] style(test): fix PHPCS double-space violations in enum tests --- tests/Phinx/Db/Adapter/PostgresAdapterTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Phinx/Db/Adapter/PostgresAdapterTest.php b/tests/Phinx/Db/Adapter/PostgresAdapterTest.php index 82496bc0d..429345957 100644 --- a/tests/Phinx/Db/Adapter/PostgresAdapterTest.php +++ b/tests/Phinx/Db/Adapter/PostgresAdapterTest.php @@ -3219,8 +3219,8 @@ public function testCreateTableWithEnumColumnAndDefault() { $table = new Table('table1', [], $this->adapter); $table->addColumn('status', 'enum', [ - 'values' => ['pending', 'active', 'archived'], - 'null' => false, + 'values' => ['pending', 'active', 'archived'], + 'null' => false, 'default' => 'pending', ])->save(); @@ -3241,7 +3241,7 @@ public function testNullableEnumColumnRoundTrip() $table = new Table('table1', [], $this->adapter); $table->addColumn('priority', 'enum', [ 'values' => ['low', 'medium', 'high'], - 'null' => true, + 'null' => true, ])->save(); $columns = $this->adapter->getColumns('table1'); @@ -3341,8 +3341,8 @@ public function testAddEnumColumnWithDefaultValue() $table->save(); $table->addColumn('role', 'enum', [ - 'values' => ['admin', 'editor', 'viewer'], - 'null' => false, + 'values' => ['admin', 'editor', 'viewer'], + 'null' => false, 'default' => 'viewer', ])->save(); @@ -3406,8 +3406,8 @@ public function testInsertDataWithEnumColumn() { $table = new Table('table1', [], $this->adapter); $table->addColumn('status', 'enum', [ - 'values' => ['pending', 'active', 'closed'], - 'null' => false, + 'values' => ['pending', 'active', 'closed'], + 'null' => false, 'default' => 'pending', ])->save();