From b158a98acc676d30ec2dcf2891472d0a481d52c6 Mon Sep 17 00:00:00 2001 From: Hein Date: Mon, 17 Aug 2026 22:36:14 +0200 Subject: [PATCH] fix(pgsql): strip backticks from column defaults in migration paths Backtick-wrapped defaults (e.g. from GORM tags like `now()`) were only stripped in the CREATE TABLE column-definition path, leaving raw backticks in the ALTER COLUMN ... SET DEFAULT migration statement and in the migration-generated CREATE TABLE template, producing invalid SQL. Default-drift comparisons also compared raw values, so a backtick-wrapped model default never matched the live DB default and kept re-emitting redundant ALTER statements. --- pkg/writers/pgsql/migration_writer.go | 21 ++++++++++++++++-- pkg/writers/pgsql/templates.go | 2 +- pkg/writers/pgsql/writer.go | 2 +- pkg/writers/pgsql/writer_test.go | 31 +++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/pkg/writers/pgsql/migration_writer.go b/pkg/writers/pgsql/migration_writer.go index a5d0587..879c79a 100644 --- a/pkg/writers/pgsql/migration_writer.go +++ b/pkg/writers/pgsql/migration_writer.go @@ -460,7 +460,7 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model } // Check default value changes - if fmt.Sprintf("%v", modelCol.Default) != fmt.Sprintf("%v", currentCol.Default) { + if !columnDefaultsEqual(modelCol.Default, currentCol.Default) { setDefault, defaultVal := formatColumnDefaultSQL(modelCol) sql, err := w.executor.ExecuteAlterColumnDefaultWithCheck(AlterColumnDefaultWithCheckData{ @@ -956,7 +956,24 @@ func columnsEqual(col1, col2 *models.Column) bool { } return columnTypesEqual(col1, col2) && col1.NotNull == col2.NotNull && - fmt.Sprintf("%v", col1.Default) == fmt.Sprintf("%v", col2.Default) + columnDefaultsEqual(col1.Default, col2.Default) +} + +// columnDefaultsEqual compares column defaults for drift detection, stripping +// MySQL-style backticks (e.g. from GORM tags) so a model default of +// "`now()`" is recognised as equal to a live default of "now()". +func columnDefaultsEqual(default1, default2 interface{}) bool { + return normalizeDefaultForCompare(default1) == normalizeDefaultForCompare(default2) +} + +func normalizeDefaultForCompare(value interface{}) string { + if value == nil { + return "" + } + if s, ok := value.(string); ok { + return strings.TrimSpace(stripBackticks(s)) + } + return fmt.Sprintf("%v", value) } func columnTypesEqual(col1, col2 *models.Column) bool { diff --git a/pkg/writers/pgsql/templates.go b/pkg/writers/pgsql/templates.go index c51f170..12a2b97 100644 --- a/pkg/writers/pgsql/templates.go +++ b/pkg/writers/pgsql/templates.go @@ -530,7 +530,7 @@ func BuildCreateTableData(schemaName string, table *models.Table) CreateTableDat } if col.Default != nil { if value, ok := col.Default.(string); ok { - colData.Default = writers.QuoteDefaultValue(value, col.Type) + colData.Default = writers.QuoteDefaultValue(stripBackticks(value), col.Type) } else { colData.Default = fmt.Sprintf("%v", col.Default) } diff --git a/pkg/writers/pgsql/writer.go b/pkg/writers/pgsql/writer.go index b09ef3a..8ad8f32 100644 --- a/pkg/writers/pgsql/writer.go +++ b/pkg/writers/pgsql/writer.go @@ -512,7 +512,7 @@ func formatColumnDefaultSQL(col *models.Column) (setDefault bool, defaultVal str return false, "" } if value, ok := col.Default.(string); ok { - return true, writers.QuoteDefaultValue(value, col.Type) + return true, writers.QuoteDefaultValue(stripBackticks(value), col.Type) } return true, fmt.Sprintf("%v", col.Default) } diff --git a/pkg/writers/pgsql/writer_test.go b/pkg/writers/pgsql/writer_test.go index 254facb..7c4dc14 100644 --- a/pkg/writers/pgsql/writer_test.go +++ b/pkg/writers/pgsql/writer_test.go @@ -1140,6 +1140,37 @@ func TestWriteSchema_EmitsGuardedAlterColumnDefaultStatements(t *testing.T) { } } +func TestWriteSchema_AlterColumnDefaultStripsBackticksFromFunctionExpression(t *testing.T) { + db := models.InitDatabase("testdb") + schema := models.InitSchema("public") + + table := models.InitTable("agent_skills", "public") + + updatedAtCol := models.InitColumn("updatedat", "agent_skills", "public") + updatedAtCol.Type = "timestamp" + updatedAtCol.Default = "`now()`" + table.Columns["updatedat"] = updatedAtCol + + schema.Tables = append(schema.Tables, table) + db.Schemas = append(db.Schemas, schema) + + var buf bytes.Buffer + writer := NewWriter(&writers.WriterOptions{}) + writer.writer = &buf + + if err := writer.WriteDatabase(db); err != nil { + t.Fatalf("WriteDatabase failed: %v", err) + } + + output := buf.String() + if strings.Contains(output, "`") { + t.Fatalf("expected no backticks in generated SQL, got:\n%s", output) + } + if !strings.Contains(output, "ALTER COLUMN updatedat SET DEFAULT now()") { + t.Fatalf("expected guarded SET DEFAULT now() without backticks, got:\n%s", output) + } +} + func TestWriteSchema_GuardedAlterColumnTypeFallsBackOnConversionFailure(t *testing.T) { db := models.InitDatabase("testdb") schema := models.InitSchema("public")