From 2cb158aefb5743308affd361339fd9829af233ba Mon Sep 17 00:00:00 2001 From: Mahmoud Almontasser Date: Sun, 3 Aug 2025 11:53:12 +0200 Subject: [PATCH 1/3] fix(postgres): resolve enum column change syntax error PostgreSQL enum column changes were failing with syntax errors because Laravel was generating invalid SQL that included CHECK constraints in ALTER COLUMN TYPE statements. Changes: - Modified PostgresGrammar::typeEnum() to return only varchar(255) for ALTER COLUMN contexts, avoiding inline CHECK constraints - Enhanced PostgresGrammar::compileChange() to handle enum columns with separate statements: DROP old constraint, ALTER column type, ADD new constraint - Added comprehensive tests for PostgreSQL enum column changes Fixes the error: "syntax error at or near 'check'" when changing enum column definitions in PostgreSQL migrations. Before: ALTER COLUMN "status" TYPE varchar(255) check ("status" in (...)) -- INVALID After: DROP CONSTRAINT IF EXISTS table_column_check; ALTER COLUMN "status" TYPE varchar(255); ADD CONSTRAINT table_column_check CHECK ("status" in (...)); -- VALID --- .../Schema/Grammars/PostgresGrammar.php | 28 +++++++++- .../DatabasePostgresSchemaGrammarTest.php | 22 ++++++++ ...resSchemaBuilderAlterTableWithEnumTest.php | 55 +++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests/Integration/Database/Postgres/DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 708e75058d1f..66b4c755f55c 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -281,6 +281,15 @@ public function compileAutoIncrementStartingValues(Blueprint $blueprint, Fluent public function compileChange(Blueprint $blueprint, Fluent $command) { $column = $command->column; + $statements = []; + + if ($column->type === 'enum') { + $constraintName = $blueprint->getTable().'_'.$column->name.'_check'; + $statements[] = sprintf('alter table %s drop constraint if exists %s', + $this->wrapTable($blueprint), + $this->wrap($constraintName) + ); + } $changes = ['type '.$this->getType($column).$this->modifyCollate($blueprint, $column)]; @@ -298,10 +307,23 @@ public function compileChange(Blueprint $blueprint, Fluent $command) } } - return sprintf('alter table %s %s', + $statements[] = sprintf('alter table %s %s', $this->wrapTable($blueprint), implode(', ', $this->prefixArray('alter column '.$this->wrap($column), $changes)) ); + + if ($column->type === 'enum') { + $constraintName = $blueprint->getTable().'_'.$column->name.'_check'; + $statements[] = sprintf('alter table %s add constraint %s check ("%s" in (%s))', + $this->wrapTable($blueprint), + $this->wrap($constraintName), + $column->name, + $this->quoteString($column->allowed) + ); + } + + // Return single statement or array of statements + return count($statements) === 1 ? $statements[0] : $statements; } /** @@ -922,6 +944,10 @@ protected function typeBoolean(Fluent $column) */ protected function typeEnum(Fluent $column) { + if ($column->change) { + return 'varchar(255)'; + } + return sprintf( 'varchar(255) check ("%s" in (%s))', $column->name, diff --git a/tests/Database/DatabasePostgresSchemaGrammarTest.php b/tests/Database/DatabasePostgresSchemaGrammarTest.php index 6a21cca7093d..79bce3527124 100755 --- a/tests/Database/DatabasePostgresSchemaGrammarTest.php +++ b/tests/Database/DatabasePostgresSchemaGrammarTest.php @@ -1201,6 +1201,28 @@ public function testDropAllDomainsWithPrefixAndSchema() $this->assertSame('drop domain "schema"."alpha", "schema"."beta", "schema"."gamma" cascade', $statement); } + public function testNativeColumnModifyingOnPostgres() + { + $blueprint = new Blueprint($this->getConnection(), 'tasks', function ($table) { + $table->enum('status', ['pending', 'queued'])->default('pending')->change(); + }); + + $statements = $blueprint->toSql(); + + $this->assertCount(4, $statements); + + $this->assertStringContainsString('drop constraint if exists "tasks_status_check"', $statements[0]); + + $this->assertStringContainsString('alter table "tasks" alter column "status" type varchar(255)', $statements[1]); + $this->assertStringNotContainsString('check (', $statements[1]); + $this->assertStringContainsString('set default \'pending\'', $statements[1]); + $this->assertStringContainsString('set not null', $statements[1]); + + $this->assertStringContainsString('add constraint "tasks_status_check" check ("status" in (\'pending\', \'queued\'))', $statements[2]); + + $this->assertStringContainsString('comment on column', $statements[3]); + } + public function testCompileColumns() { $connection = $this->getConnection(); diff --git a/tests/Integration/Database/Postgres/DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php b/tests/Integration/Database/Postgres/DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php new file mode 100644 index 000000000000..582331f8d363 --- /dev/null +++ b/tests/Integration/Database/Postgres/DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php @@ -0,0 +1,55 @@ +integer('id'); + $table->string('name'); + $table->enum('status', ['pending', 'processing'])->default('pending'); + }); + } + + protected function destroyDatabaseMigrations() + { + Schema::drop('orders'); + } + + public function testChangeEnumColumnValues() + { + Schema::table('orders', function (Blueprint $table) { + $table->enum('status', ['pending', 'queued'])->default('pending')->change(); + }); + + $this->assertTrue(Schema::hasColumn('orders', 'status')); + $this->assertSame('character varying', Schema::getColumnType('orders', 'status')); + } + + public function testRenameColumnOnTableWithEnum() + { + Schema::table('orders', function (Blueprint $table) { + $table->renameColumn('name', 'title'); + }); + + $this->assertTrue(Schema::hasColumn('orders', 'title')); + } + + public function testChangeNonEnumColumnOnTableWithEnum() + { + Schema::table('orders', function (Blueprint $table) { + $table->unsignedInteger('id')->change(); + }); + + $this->assertSame('integer', Schema::getColumnType('orders', 'id')); + } +} From ce7d44daf294b0046f938c330ff454ff229b6ed9 Mon Sep 17 00:00:00 2001 From: Mahmoud Almontasser Date: Sun, 3 Aug 2025 12:15:40 +0200 Subject: [PATCH 2/3] fix(tests): handle PostgreSQL column type name variations PostgreSQL may return different column type names (e.g., 'varchar' vs 'character varying', 'int4' vs 'integer') depending on configuration. Updated assertions to use assertContains with arrays of valid type names to ensure tests pass consistently across different PostgreSQL setups. --- .../DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Integration/Database/Postgres/DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php b/tests/Integration/Database/Postgres/DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php index 582331f8d363..d74e54a28af3 100644 --- a/tests/Integration/Database/Postgres/DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php +++ b/tests/Integration/Database/Postgres/DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php @@ -32,7 +32,7 @@ public function testChangeEnumColumnValues() }); $this->assertTrue(Schema::hasColumn('orders', 'status')); - $this->assertSame('character varying', Schema::getColumnType('orders', 'status')); + $this->assertContains(Schema::getColumnType('orders', 'status'), ['varchar', 'character varying']); } public function testRenameColumnOnTableWithEnum() @@ -50,6 +50,6 @@ public function testChangeNonEnumColumnOnTableWithEnum() $table->unsignedInteger('id')->change(); }); - $this->assertSame('integer', Schema::getColumnType('orders', 'id')); + $this->assertContains(Schema::getColumnType('orders', 'id'), ['integer', 'int4']); } } From f2acf119392caefed57898424169cdd68c481ac6 Mon Sep 17 00:00:00 2001 From: Mahmoud Almontasser Date: Sun, 3 Aug 2025 12:26:38 +0200 Subject: [PATCH 3/3] test: use specific PostgreSQL column type names in assertions --- .../DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Integration/Database/Postgres/DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php b/tests/Integration/Database/Postgres/DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php index d74e54a28af3..7319ca9d16a7 100644 --- a/tests/Integration/Database/Postgres/DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php +++ b/tests/Integration/Database/Postgres/DatabasePostgresSchemaBuilderAlterTableWithEnumTest.php @@ -32,7 +32,7 @@ public function testChangeEnumColumnValues() }); $this->assertTrue(Schema::hasColumn('orders', 'status')); - $this->assertContains(Schema::getColumnType('orders', 'status'), ['varchar', 'character varying']); + $this->assertSame('varchar', Schema::getColumnType('orders', 'status')); } public function testRenameColumnOnTableWithEnum() @@ -50,6 +50,6 @@ public function testChangeNonEnumColumnOnTableWithEnum() $table->unsignedInteger('id')->change(); }); - $this->assertContains(Schema::getColumnType('orders', 'id'), ['integer', 'int4']); + $this->assertSame('int4', Schema::getColumnType('orders', 'id')); } }