fix(pgsql): handle nullability/type/default drift on existing columns

Existing databases that already ran an old migration kept stale NOT
NULL constraints and mismatched column types/defaults, because the
schema writer only emitted idempotent ADD COLUMN IF NOT EXISTS guards
and never altered columns that already existed.

- Emit guarded ALTER COLUMN ... SET/DROP NOT NULL when a column's
  nullability differs from the model.
- Emit guarded ALTER COLUMN ... TYPE, falling back to renaming the old
  column and adding a fresh one when the in-place conversion fails.
- Emit guarded ALTER COLUMN ... SET/DROP DEFAULT for default drift.
- Collapse the previously duplicated plain/guarded templates so
  WriteSchema (full-schema, live-state-checking) and WriteMigration
  (diff-based) share the same guarded SQL templates and Go helpers
  instead of maintaining the logic twice.
This commit is contained in:
Hein
2026-08-14 16:17:17 +02:00
parent e650406177
commit d84306934a
11 changed files with 275 additions and 137 deletions
+71
View File
@@ -1106,6 +1106,40 @@ func TestWriteSchema_EmitsGuardedAlterColumnTypeStatements(t *testing.T) {
}
}
func TestWriteSchema_EmitsGuardedAlterColumnDefaultStatements(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
table := models.InitTable("agent_skills", "public")
statusCol := models.InitColumn("status", "agent_skills", "public")
statusCol.Type = "text"
statusCol.Default = "active"
table.Columns["status"] = statusCol
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, "-- Alter column defaults for schema: public") {
t.Fatalf("expected alter column default section, got:\n%s", output)
}
if !strings.Contains(output, "pg_get_expr(d.adbin, d.adrelid)") {
t.Fatalf("expected guarded live-default check, got:\n%s", output)
}
if !strings.Contains(output, "ALTER COLUMN status SET DEFAULT 'active'") {
t.Fatalf("expected guarded SET DEFAULT for status column, got:\n%s", output)
}
}
func TestWriteSchema_GuardedAlterColumnTypeFallsBackOnConversionFailure(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")
@@ -1142,6 +1176,43 @@ func TestWriteSchema_GuardedAlterColumnTypeFallsBackOnConversionFailure(t *testi
}
}
func TestWriteSchema_EmitsGuardedAlterColumnNullabilityStatements(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("origin")
table := models.InitTable("service_instance", "origin")
typeCol := models.InitColumn("rid_service_instance_type", "service_instance", "origin")
typeCol.Type = "text"
typeCol.NotNull = false
table.Columns["rid_service_instance_type"] = typeCol
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, "-- Alter column nullability for schema: origin") {
t.Fatalf("expected alter column nullability section, got:\n%s", output)
}
if !strings.Contains(output, "a.attnotnull") {
t.Fatalf("expected guarded live-nullability check, got:\n%s", output)
}
if !strings.Contains(output, "current_not_null IS DISTINCT FROM false") {
t.Fatalf("expected guard comparing live nullability against desired value, got:\n%s", output)
}
if !strings.Contains(output, "ALTER COLUMN rid_service_instance_type DROP NOT NULL") {
t.Fatalf("expected guarded DROP NOT NULL for nullable column, got:\n%s", output)
}
}
func TestWriteSchema_UsesStorageTypeForSerialAlterStatements(t *testing.T) {
db := models.InitDatabase("testdb")
schema := models.InitSchema("public")