Compare commits

...
2 Commits
Author SHA1 Message Date
Hein 465db7643c chore(release): update package version to 1.0.68
Release / test (push) Successful in 3m8s
Release / release (push) Successful in 4m18s
Release / pkg-deb (push) Successful in 50s
Release / pkg-aur (push) Successful in 1m2s
Release / pkg-rpm (push) Successful in 2m8s
2026-08-14 16:17:43 +02:00
Hein d84306934a 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.
2026-08-14 16:17:17 +02:00
13 changed files with 277 additions and 139 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Maintainer: Hein (Warky Devs) <hein@warky.dev> # Maintainer: Hein (Warky Devs) <hein@warky.dev>
pkgname=relspec pkgname=relspec
pkgver=1.0.67 pkgver=1.0.68
pkgrel=1 pkgrel=1
pkgdesc="RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs." pkgdesc="RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs."
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
+1 -1
View File
@@ -1,5 +1,5 @@
Name: relspec Name: relspec
Version: 1.0.67 Version: 1.0.68
Release: 1%{?dist} Release: 1%{?dist}
Summary: RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs. Summary: RelSpec is a comprehensive database relations management tool that reads, transforms, and writes database table specifications across multiple formats and ORMs.
+11 -52
View File
@@ -409,14 +409,7 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model
if !exists { if !exists {
// Column doesn't exist, add it // Column doesn't exist, add it
defaultVal := "" _, defaultVal := formatColumnDefaultSQL(modelCol)
if modelCol.Default != nil {
if value, ok := modelCol.Default.(string); ok {
defaultVal = writers.QuoteDefaultValue(value, modelCol.Type)
} else {
defaultVal = fmt.Sprintf("%v", modelCol.Default)
}
}
sql, err := w.executor.ExecuteAddColumn(AddColumnData{ sql, err := w.executor.ExecuteAddColumn(AddColumnData{
SchemaName: schema.Name, SchemaName: schema.Name,
@@ -443,13 +436,13 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model
// Column exists but properties changed // Column exists but properties changed
if !columnTypesEqual(modelCol, currentCol) { if !columnTypesEqual(modelCol, currentCol) {
newType := effectiveAlterColumnSQLType(modelCol) newType := effectiveAlterColumnSQLType(modelCol)
sql, err := w.executor.ExecuteAlterColumnTypeWithFallback(AlterColumnTypeWithFallbackData{ sql, err := w.executor.ExecuteAlterColumnTypeWithCheck(AlterColumnTypeWithCheckData{
SchemaName: schema.Name, SchemaName: schema.Name,
TableName: modelTable.Name, TableName: modelTable.Name,
ColumnName: modelCol.Name, ColumnName: modelCol.Name,
NewType: newType, NewType: newType,
UsingExpr: buildAlterColumnUsingExpression(modelCol.Name, newType), EquivalentTypes: equivalentTypeListSQL(newType),
OldColumnName: renamedColumnName(modelCol.Name, effectiveAlterColumnSQLType(currentCol)), UsingExpr: buildAlterColumnUsingExpression(modelCol.Name, newType),
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -468,17 +461,9 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model
// Check default value changes // Check default value changes
if fmt.Sprintf("%v", modelCol.Default) != fmt.Sprintf("%v", currentCol.Default) { if fmt.Sprintf("%v", modelCol.Default) != fmt.Sprintf("%v", currentCol.Default) {
setDefault := modelCol.Default != nil setDefault, defaultVal := formatColumnDefaultSQL(modelCol)
defaultVal := ""
if setDefault {
if value, ok := modelCol.Default.(string); ok {
defaultVal = writers.QuoteDefaultValue(value, modelCol.Type)
} else {
defaultVal = fmt.Sprintf("%v", modelCol.Default)
}
}
sql, err := w.executor.ExecuteAlterColumnDefault(AlterColumnDefaultData{ sql, err := w.executor.ExecuteAlterColumnDefaultWithCheck(AlterColumnDefaultWithCheckData{
SchemaName: schema.Name, SchemaName: schema.Name,
TableName: modelTable.Name, TableName: modelTable.Name,
ColumnName: modelCol.Name, ColumnName: modelCol.Name,
@@ -502,7 +487,7 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model
// Check nullability changes // Check nullability changes
if modelCol.NotNull != currentCol.NotNull { if modelCol.NotNull != currentCol.NotNull {
sql, err := w.executor.ExecuteAlterColumnNullability(AlterColumnNullabilityData{ sql, err := w.executor.ExecuteAlterColumnNullabilityWithCheck(AlterColumnNullabilityWithCheckData{
SchemaName: schema.Name, SchemaName: schema.Name,
TableName: modelTable.Name, TableName: modelTable.Name,
ColumnName: modelCol.Name, ColumnName: modelCol.Name,
@@ -964,32 +949,6 @@ func (w *MigrationWriter) generateAuditScripts(schema *models.Schema, auditConfi
// Helper functions for comparing database objects // Helper functions for comparing database objects
// renamedColumnName builds the fallback column name used when an in-place
// type conversion fails: "<column>_<oldtype>", with the old type sanitized
// to a valid identifier fragment (e.g. "varchar(50)" -> "varchar_50").
func renamedColumnName(columnName, oldType string) string {
sanitized := strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
return r
case r >= 'A' && r <= 'Z':
return r + ('a' - 'A')
default:
return '_'
}
}, oldType)
for strings.Contains(sanitized, "__") {
sanitized = strings.ReplaceAll(sanitized, "__", "_")
}
sanitized = strings.Trim(sanitized, "_")
if sanitized == "" {
return columnName + "_old"
}
return columnName + "_" + sanitized
}
// columnsEqual checks if two columns have the same definition // columnsEqual checks if two columns have the same definition
func columnsEqual(col1, col2 *models.Column) bool { func columnsEqual(col1, col2 *models.Column) bool {
if col1 == nil || col2 == nil { if col1 == nil || col2 == nil {
+5 -2
View File
@@ -170,8 +170,11 @@ func TestWriteMigration_AltersColumnTypeFallsBackToRenameAndAddOnConversionFailu
if !strings.Contains(output, "EXCEPTION WHEN OTHERS THEN") { if !strings.Contains(output, "EXCEPTION WHEN OTHERS THEN") {
t.Fatalf("expected migration to guard the type conversion with an exception handler, got:\n%s", output) t.Fatalf("expected migration to guard the type conversion with an exception handler, got:\n%s", output)
} }
if !strings.Contains(output, "RENAME COLUMN details TO details_varchar_50") { if !strings.Contains(output, "RENAME COLUMN details TO %I") {
t.Fatalf("expected migration to rename the old column on conversion failure, got:\n%s", output) t.Fatalf("expected migration to rename the old column (derived from the live type) on conversion failure, got:\n%s", output)
}
if !strings.Contains(output, "renamed_column := 'details_' || trim(both '_' from regexp_replace(lower(current_type)") {
t.Fatalf("expected migration to derive the renamed column name from the live type, got:\n%s", output)
} }
if !strings.Contains(output, "ADD COLUMN details integer") { if !strings.Contains(output, "ADD COLUMN details integer") {
t.Fatalf("expected migration to add a fresh column with the new type on conversion failure, got:\n%s", output) t.Fatalf("expected migration to add a fresh column with the new type on conversion failure, got:\n%s", output)
+26 -54
View File
@@ -89,27 +89,10 @@ type AddColumnData struct {
NotNull bool NotNull bool
} }
// AlterColumnTypeData contains data for alter column type template // AlterColumnTypeWithCheckData contains data for the guarded alter column
type AlterColumnTypeData struct { // type template, which only alters existing columns whose live type
SchemaName string // differs from the desired one, and falls back to renaming the old column
TableName string // and adding a fresh one when the in-place conversion is not possible.
ColumnName string
NewType string
UsingExpr string
}
// AlterColumnTypeWithFallbackData contains data for the alter column type
// template that falls back to renaming the old column and adding a fresh
// one when the in-place type conversion is not possible.
type AlterColumnTypeWithFallbackData struct {
SchemaName string
TableName string
ColumnName string
NewType string
UsingExpr string
OldColumnName string
}
type AlterColumnTypeWithCheckData struct { type AlterColumnTypeWithCheckData struct {
SchemaName string SchemaName string
TableName string TableName string
@@ -119,8 +102,10 @@ type AlterColumnTypeWithCheckData struct {
UsingExpr string UsingExpr string
} }
// AlterColumnDefaultData contains data for alter column default template // AlterColumnDefaultWithCheckData contains data for the guarded alter
type AlterColumnDefaultData struct { // column default template, which only alters existing columns whose live
// default differs from the desired one.
type AlterColumnDefaultWithCheckData struct {
SchemaName string SchemaName string
TableName string TableName string
ColumnName string ColumnName string
@@ -128,8 +113,10 @@ type AlterColumnDefaultData struct {
DefaultValue string DefaultValue string
} }
// AlterColumnNullabilityData contains data for alter column nullability template // AlterColumnNullabilityWithCheckData contains data for the guarded alter
type AlterColumnNullabilityData struct { // column nullability template, which only alters existing columns whose
// live NOT NULL state differs from the desired one.
type AlterColumnNullabilityWithCheckData struct {
SchemaName string SchemaName string
TableName string TableName string
ColumnName string ColumnName string
@@ -322,27 +309,9 @@ func (te *TemplateExecutor) ExecuteAddColumn(data AddColumnData) (string, error)
return buf.String(), nil return buf.String(), nil
} }
// ExecuteAlterColumnType executes the alter column type template // ExecuteAlterColumnTypeWithCheck executes the guarded alter column type
func (te *TemplateExecutor) ExecuteAlterColumnType(data AlterColumnTypeData) (string, error) { // template shared by the full-schema writer and the diff-based migration
var buf bytes.Buffer // writer.
err := te.templates.ExecuteTemplate(&buf, "alter_column_type.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute alter_column_type template: %w", err)
}
return buf.String(), nil
}
// ExecuteAlterColumnTypeWithFallback executes the alter column type template
// that renames the old column and adds a new one when the conversion fails.
func (te *TemplateExecutor) ExecuteAlterColumnTypeWithFallback(data AlterColumnTypeWithFallbackData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "alter_column_type_with_fallback.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute alter_column_type_with_fallback template: %w", err)
}
return buf.String(), nil
}
func (te *TemplateExecutor) ExecuteAlterColumnTypeWithCheck(data AlterColumnTypeWithCheckData) (string, error) { func (te *TemplateExecutor) ExecuteAlterColumnTypeWithCheck(data AlterColumnTypeWithCheckData) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "alter_column_type_with_check.tmpl", data) err := te.templates.ExecuteTemplate(&buf, "alter_column_type_with_check.tmpl", data)
@@ -352,22 +321,25 @@ func (te *TemplateExecutor) ExecuteAlterColumnTypeWithCheck(data AlterColumnType
return buf.String(), nil return buf.String(), nil
} }
// ExecuteAlterColumnDefault executes the alter column default template // ExecuteAlterColumnDefaultWithCheck executes the guarded alter column
func (te *TemplateExecutor) ExecuteAlterColumnDefault(data AlterColumnDefaultData) (string, error) { // default template shared by the full-schema writer and the diff-based
// migration writer.
func (te *TemplateExecutor) ExecuteAlterColumnDefaultWithCheck(data AlterColumnDefaultWithCheckData) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "alter_column_default.tmpl", data) err := te.templates.ExecuteTemplate(&buf, "alter_column_default_with_check.tmpl", data)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to execute alter_column_default template: %w", err) return "", fmt.Errorf("failed to execute alter_column_default_with_check template: %w", err)
} }
return buf.String(), nil return buf.String(), nil
} }
// ExecuteAlterColumnNullability executes the alter column nullability template // ExecuteAlterColumnNullabilityWithCheck executes the guarded alter column
func (te *TemplateExecutor) ExecuteAlterColumnNullability(data AlterColumnNullabilityData) (string, error) { // nullability template.
func (te *TemplateExecutor) ExecuteAlterColumnNullabilityWithCheck(data AlterColumnNullabilityWithCheckData) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "alter_column_nullability.tmpl", data) err := te.templates.ExecuteTemplate(&buf, "alter_column_nullability_with_check.tmpl", data)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to execute alter_column_nullability template: %w", err) return "", fmt.Errorf("failed to execute alter_column_nullability_with_check template: %w", err)
} }
return buf.String(), nil return buf.String(), nil
} }
@@ -1,7 +0,0 @@
{{- if .SetDefault -}}
ALTER TABLE {{qual_table .SchemaName .TableName}}
ALTER COLUMN {{quote_ident .ColumnName}} SET DEFAULT {{.DefaultValue}};
{{- else -}}
ALTER TABLE {{qual_table .SchemaName .TableName}}
ALTER COLUMN {{quote_ident .ColumnName}} DROP DEFAULT;
{{- end -}}
@@ -0,0 +1,29 @@
DO $$
DECLARE
current_default text;
BEGIN
SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid)
INTO current_default
FROM pg_attribute a
JOIN pg_class t ON t.oid = a.attrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
WHERE n.nspname = '{{.SchemaName}}'
AND t.relname = '{{.TableName}}'
AND a.attname = '{{.ColumnName}}'
AND a.attnum > 0
AND NOT a.attisdropped;
{{- if .SetDefault }}
IF current_default IS DISTINCT FROM {{quote .DefaultValue}} THEN
ALTER TABLE {{qual_table .SchemaName .TableName}}
ALTER COLUMN {{quote_ident .ColumnName}} SET DEFAULT {{.DefaultValue}};
END IF;
{{- else }}
IF current_default IS NOT NULL THEN
ALTER TABLE {{qual_table .SchemaName .TableName}}
ALTER COLUMN {{quote_ident .ColumnName}} DROP DEFAULT;
END IF;
{{- end }}
END;
$$;
@@ -1,7 +0,0 @@
{{- if .NotNull -}}
ALTER TABLE {{qual_table .SchemaName .TableName}}
ALTER COLUMN {{quote_ident .ColumnName}} SET NOT NULL;
{{- else -}}
ALTER TABLE {{qual_table .SchemaName .TableName}}
ALTER COLUMN {{quote_ident .ColumnName}} DROP NOT NULL;
{{- end -}}
@@ -0,0 +1,26 @@
DO $$
DECLARE
current_not_null boolean;
BEGIN
SELECT a.attnotnull
INTO current_not_null
FROM pg_attribute a
JOIN pg_class t ON t.oid = a.attrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE n.nspname = '{{.SchemaName}}'
AND t.relname = '{{.TableName}}'
AND a.attname = '{{.ColumnName}}'
AND a.attnum > 0
AND NOT a.attisdropped;
IF current_not_null IS NOT NULL AND current_not_null IS DISTINCT FROM {{.NotNull}} THEN
{{- if .NotNull }}
ALTER TABLE {{qual_table .SchemaName .TableName}}
ALTER COLUMN {{quote_ident .ColumnName}} SET NOT NULL;
{{- else }}
ALTER TABLE {{qual_table .SchemaName .TableName}}
ALTER COLUMN {{quote_ident .ColumnName}} DROP NOT NULL;
{{- end }}
END IF;
END;
$$;
@@ -1,2 +0,0 @@
ALTER TABLE {{qual_table .SchemaName .TableName}}
ALTER COLUMN {{quote_ident .ColumnName}} TYPE {{.NewType}}{{if .UsingExpr}} USING {{.UsingExpr}}{{end}};
@@ -1,13 +0,0 @@
DO $$
BEGIN
BEGIN
ALTER TABLE {{qual_table .SchemaName .TableName}}
ALTER COLUMN {{quote_ident .ColumnName}} TYPE {{.NewType}}{{if .UsingExpr}} USING {{.UsingExpr}}{{end}};
EXCEPTION WHEN OTHERS THEN
ALTER TABLE {{qual_table .SchemaName .TableName}}
RENAME COLUMN {{quote_ident .ColumnName}} TO {{quote_ident .OldColumnName}};
ALTER TABLE {{qual_table .SchemaName .TableName}}
ADD COLUMN {{quote_ident .ColumnName}} {{.NewType}};
END;
END;
$$;
+107
View File
@@ -475,6 +475,75 @@ func (w *Writer) GenerateAlterColumnTypeStatements(schema *models.Schema) ([]str
return statements, nil return statements, nil
} }
// GenerateAlterColumnDefaultStatements generates guarded ALTER TABLE
// statements to bring existing columns' DEFAULT clause in line with the
// model, safe to run against a database that already has the columns.
func (w *Writer) GenerateAlterColumnDefaultStatements(schema *models.Schema) ([]string, error) {
statements := []string{}
statements = append(statements, fmt.Sprintf("-- Alter column defaults for schema: %s", schema.Name))
for _, table := range schema.Tables {
columns := getSortedColumns(table.Columns)
for _, col := range columns {
setDefault, defaultVal := formatColumnDefaultSQL(col)
stmt, err := w.executor.ExecuteAlterColumnDefaultWithCheck(AlterColumnDefaultWithCheckData{
SchemaName: schema.Name,
TableName: table.Name,
ColumnName: col.Name,
SetDefault: setDefault,
DefaultValue: defaultVal,
})
if err != nil {
return nil, fmt.Errorf("failed to generate alter column default for %s.%s.%s: %w", schema.Name, table.Name, col.Name, err)
}
statements = append(statements, stmt)
}
}
return statements, nil
}
// formatColumnDefaultSQL renders a column's model-level default into the
// SQL literal/expression used by ALTER COLUMN ... SET DEFAULT, shared by
// the full-schema writer and the diff-based migration writer.
func formatColumnDefaultSQL(col *models.Column) (setDefault bool, defaultVal string) {
if col.Default == nil {
return false, ""
}
if value, ok := col.Default.(string); ok {
return true, writers.QuoteDefaultValue(value, col.Type)
}
return true, fmt.Sprintf("%v", col.Default)
}
// GenerateAlterColumnNullabilityStatements generates guarded ALTER TABLE
// statements to bring existing columns' NOT NULL state in line with the
// model, safe to run against a database that already has the columns.
func (w *Writer) GenerateAlterColumnNullabilityStatements(schema *models.Schema) ([]string, error) {
statements := []string{}
statements = append(statements, fmt.Sprintf("-- Alter column nullability for schema: %s", schema.Name))
for _, table := range schema.Tables {
columns := getSortedColumns(table.Columns)
for _, col := range columns {
stmt, err := w.executor.ExecuteAlterColumnNullabilityWithCheck(AlterColumnNullabilityWithCheckData{
SchemaName: schema.Name,
TableName: table.Name,
ColumnName: col.Name,
NotNull: col.NotNull,
})
if err != nil {
return nil, fmt.Errorf("failed to generate alter column nullability for %s.%s.%s: %w", schema.Name, table.Name, col.Name, err)
}
statements = append(statements, stmt)
}
}
return statements, nil
}
// GenerateAddColumnsForDatabase generates ALTER TABLE ADD COLUMN statements for the entire database // GenerateAddColumnsForDatabase generates ALTER TABLE ADD COLUMN statements for the entire database
func (w *Writer) GenerateAddColumnsForDatabase(db *models.Database) ([]string, error) { func (w *Writer) GenerateAddColumnsForDatabase(db *models.Database) ([]string, error) {
statements := []string{} statements := []string{}
@@ -641,6 +710,14 @@ func (w *Writer) WriteSchema(schema *models.Schema) error {
return err return err
} }
if err := w.writeAlterColumnDefaults(schema); err != nil {
return err
}
if err := w.writeAlterColumnNullability(schema); err != nil {
return err
}
// Phase 4: Create primary keys (priority 160) // Phase 4: Create primary keys (priority 160)
if err := w.writePrimaryKeys(schema); err != nil { if err := w.writePrimaryKeys(schema); err != nil {
return err return err
@@ -859,6 +936,36 @@ func (w *Writer) writeAlterColumnTypes(schema *models.Schema) error {
return nil return nil
} }
func (w *Writer) writeAlterColumnDefaults(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Alter column defaults for schema: %s\n", schema.Name)
statements, err := w.GenerateAlterColumnDefaultStatements(schema)
if err != nil {
return err
}
for _, stmt := range statements[1:] {
fmt.Fprint(w.writer, stmt)
fmt.Fprint(w.writer, "\n")
}
return nil
}
func (w *Writer) writeAlterColumnNullability(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Alter column nullability for schema: %s\n", schema.Name)
statements, err := w.GenerateAlterColumnNullabilityStatements(schema)
if err != nil {
return err
}
for _, stmt := range statements[1:] {
fmt.Fprint(w.writer, stmt)
fmt.Fprint(w.writer, "\n")
}
return nil
}
// writePrimaryKeys generates ALTER TABLE statements for primary keys // writePrimaryKeys generates ALTER TABLE statements for primary keys
func (w *Writer) writePrimaryKeys(schema *models.Schema) error { func (w *Writer) writePrimaryKeys(schema *models.Schema) error {
fmt.Fprintf(w.writer, "-- Primary keys for schema: %s\n", schema.Name) fmt.Fprintf(w.writer, "-- Primary keys for schema: %s\n", schema.Name)
+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) { func TestWriteSchema_GuardedAlterColumnTypeFallsBackOnConversionFailure(t *testing.T) {
db := models.InitDatabase("testdb") db := models.InitDatabase("testdb")
schema := models.InitSchema("public") 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) { func TestWriteSchema_UsesStorageTypeForSerialAlterStatements(t *testing.T) {
db := models.InitDatabase("testdb") db := models.InitDatabase("testdb")
schema := models.InitSchema("public") schema := models.InitSchema("public")