Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
465db7643c | ||
|
|
d84306934a | ||
|
|
e650406177 | ||
|
|
fc3409f324 | ||
|
|
97139723c9 |
+1
-1
@@ -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.66
|
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,5 +1,5 @@
|
|||||||
Name: relspec
|
Name: relspec
|
||||||
Version: 1.0.66
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -442,12 +435,14 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model
|
|||||||
} else if !columnsEqual(modelCol, currentCol) {
|
} else if !columnsEqual(modelCol, currentCol) {
|
||||||
// Column exists but properties changed
|
// Column exists but properties changed
|
||||||
if !columnTypesEqual(modelCol, currentCol) {
|
if !columnTypesEqual(modelCol, currentCol) {
|
||||||
sql, err := w.executor.ExecuteAlterColumnType(AlterColumnTypeData{
|
newType := effectiveAlterColumnSQLType(modelCol)
|
||||||
SchemaName: schema.Name,
|
sql, err := w.executor.ExecuteAlterColumnTypeWithCheck(AlterColumnTypeWithCheckData{
|
||||||
TableName: modelTable.Name,
|
SchemaName: schema.Name,
|
||||||
ColumnName: modelCol.Name,
|
TableName: modelTable.Name,
|
||||||
NewType: effectiveAlterColumnSQLType(modelCol),
|
ColumnName: modelCol.Name,
|
||||||
UsingExpr: buildAlterColumnUsingExpression(modelCol.Name, effectiveAlterColumnSQLType(modelCol)),
|
NewType: newType,
|
||||||
|
EquivalentTypes: equivalentTypeListSQL(newType),
|
||||||
|
UsingExpr: buildAlterColumnUsingExpression(modelCol.Name, newType),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -466,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,
|
||||||
@@ -497,6 +484,29 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model
|
|||||||
}
|
}
|
||||||
scripts = append(scripts, script)
|
scripts = append(scripts, script)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check nullability changes
|
||||||
|
if modelCol.NotNull != currentCol.NotNull {
|
||||||
|
sql, err := w.executor.ExecuteAlterColumnNullabilityWithCheck(AlterColumnNullabilityWithCheckData{
|
||||||
|
SchemaName: schema.Name,
|
||||||
|
TableName: modelTable.Name,
|
||||||
|
ColumnName: modelCol.Name,
|
||||||
|
NotNull: modelCol.NotNull,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
script := MigrationScript{
|
||||||
|
ObjectName: fmt.Sprintf("%s.%s.%s", schema.Name, modelTable.Name, modelCol.Name),
|
||||||
|
ObjectType: "alter column nullability",
|
||||||
|
Schema: schema.Name,
|
||||||
|
Priority: 145,
|
||||||
|
Sequence: len(scripts),
|
||||||
|
Body: sql,
|
||||||
|
}
|
||||||
|
scripts = append(scripts, script)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,89 @@ func TestWriteMigration_AltersColumnTypeWhenActualTypeDiffers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWriteMigration_AltersColumnTypeFallsBackToRenameAndAddOnConversionFailure(t *testing.T) {
|
||||||
|
current := models.InitDatabase("testdb")
|
||||||
|
currentSchema := models.InitSchema("public")
|
||||||
|
currentTable := models.InitTable("learnings", "public")
|
||||||
|
currentDetails := models.InitColumn("details", "learnings", "public")
|
||||||
|
currentDetails.Type = "varchar(50)"
|
||||||
|
currentTable.Columns["details"] = currentDetails
|
||||||
|
currentSchema.Tables = append(currentSchema.Tables, currentTable)
|
||||||
|
current.Schemas = append(current.Schemas, currentSchema)
|
||||||
|
|
||||||
|
model := models.InitDatabase("testdb")
|
||||||
|
modelSchema := models.InitSchema("public")
|
||||||
|
modelTable := models.InitTable("learnings", "public")
|
||||||
|
modelDetails := models.InitColumn("details", "learnings", "public")
|
||||||
|
modelDetails.Type = "integer"
|
||||||
|
modelTable.Columns["details"] = modelDetails
|
||||||
|
modelSchema.Tables = append(modelSchema.Tables, modelTable)
|
||||||
|
model.Schemas = append(model.Schemas, modelSchema)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
writer, err := NewMigrationWriter(&writers.WriterOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create writer: %v", err)
|
||||||
|
}
|
||||||
|
writer.writer = &buf
|
||||||
|
|
||||||
|
if err := writer.WriteMigration(model, current); err != nil {
|
||||||
|
t.Fatalf("WriteMigration failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "RENAME COLUMN details TO %I") {
|
||||||
|
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") {
|
||||||
|
t.Fatalf("expected migration to add a fresh column with the new type on conversion failure, got:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteMigration_AltersColumnNullabilityWhenNotNullDiffers(t *testing.T) {
|
||||||
|
current := models.InitDatabase("testdb")
|
||||||
|
currentSchema := models.InitSchema("public")
|
||||||
|
currentTable := models.InitTable("service_instance", "public")
|
||||||
|
currentType := models.InitColumn("rid_service_instance_type", "service_instance", "public")
|
||||||
|
currentType.Type = "text"
|
||||||
|
currentType.NotNull = true
|
||||||
|
currentTable.Columns["rid_service_instance_type"] = currentType
|
||||||
|
currentSchema.Tables = append(currentSchema.Tables, currentTable)
|
||||||
|
current.Schemas = append(current.Schemas, currentSchema)
|
||||||
|
|
||||||
|
model := models.InitDatabase("testdb")
|
||||||
|
modelSchema := models.InitSchema("public")
|
||||||
|
modelTable := models.InitTable("service_instance", "public")
|
||||||
|
modelType := models.InitColumn("rid_service_instance_type", "service_instance", "public")
|
||||||
|
modelType.Type = "text"
|
||||||
|
modelType.NotNull = false
|
||||||
|
modelTable.Columns["rid_service_instance_type"] = modelType
|
||||||
|
modelSchema.Tables = append(modelSchema.Tables, modelTable)
|
||||||
|
model.Schemas = append(model.Schemas, modelSchema)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
writer, err := NewMigrationWriter(&writers.WriterOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create writer: %v", err)
|
||||||
|
}
|
||||||
|
writer.writer = &buf
|
||||||
|
|
||||||
|
if err := writer.WriteMigration(model, current); err != nil {
|
||||||
|
t.Fatalf("WriteMigration failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, "ALTER COLUMN rid_service_instance_type DROP NOT NULL") {
|
||||||
|
t.Fatalf("expected migration to drop NOT NULL on existing column, got:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWriteMigration_UsesStorageTypeForSerialAlterStatements(t *testing.T) {
|
func TestWriteMigration_UsesStorageTypeForSerialAlterStatements(t *testing.T) {
|
||||||
current := models.InitDatabase("testdb")
|
current := models.InitDatabase("testdb")
|
||||||
currentSchema := models.InitSchema("public")
|
currentSchema := models.InitSchema("public")
|
||||||
|
|||||||
@@ -89,15 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
type AlterColumnTypeWithCheckData struct {
|
type AlterColumnTypeWithCheckData struct {
|
||||||
SchemaName string
|
SchemaName string
|
||||||
TableName string
|
TableName string
|
||||||
@@ -107,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
|
||||||
@@ -116,6 +113,16 @@ type AlterColumnDefaultData struct {
|
|||||||
DefaultValue string
|
DefaultValue string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AlterColumnNullabilityWithCheckData contains data for the guarded alter
|
||||||
|
// column nullability template, which only alters existing columns whose
|
||||||
|
// live NOT NULL state differs from the desired one.
|
||||||
|
type AlterColumnNullabilityWithCheckData struct {
|
||||||
|
SchemaName string
|
||||||
|
TableName string
|
||||||
|
ColumnName string
|
||||||
|
NotNull bool
|
||||||
|
}
|
||||||
|
|
||||||
// CreatePrimaryKeyData contains data for create primary key template
|
// CreatePrimaryKeyData contains data for create primary key template
|
||||||
type CreatePrimaryKeyData struct {
|
type CreatePrimaryKeyData struct {
|
||||||
SchemaName string
|
SchemaName string
|
||||||
@@ -302,16 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -321,12 +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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteAlterColumnNullabilityWithCheck executes the guarded alter column
|
||||||
|
// nullability template.
|
||||||
|
func (te *TemplateExecutor) ExecuteAlterColumnNullabilityWithCheck(data AlterColumnNullabilityWithCheckData) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := te.templates.ExecuteTemplate(&buf, "alter_column_nullability_with_check.tmpl", data)
|
||||||
|
if err != nil {
|
||||||
|
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;
|
||||||
|
$$;
|
||||||
@@ -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,6 +1,7 @@
|
|||||||
DO $$
|
DO $$
|
||||||
DECLARE
|
DECLARE
|
||||||
current_type text;
|
current_type text;
|
||||||
|
renamed_column text;
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT pg_catalog.format_type(a.atttypid, a.atttypmod)
|
SELECT pg_catalog.format_type(a.atttypid, a.atttypmod)
|
||||||
INTO current_type
|
INTO current_type
|
||||||
@@ -15,8 +16,15 @@ BEGIN
|
|||||||
|
|
||||||
IF current_type IS NOT NULL
|
IF current_type IS NOT NULL
|
||||||
AND current_type <> ALL(ARRAY[{{.EquivalentTypes}}]) THEN
|
AND current_type <> ALL(ARRAY[{{.EquivalentTypes}}]) THEN
|
||||||
ALTER TABLE {{qual_table .SchemaName .TableName}}
|
BEGIN
|
||||||
ALTER COLUMN {{quote_ident .ColumnName}} TYPE {{.NewType}}{{if .UsingExpr}} USING {{.UsingExpr}}{{end}};
|
ALTER TABLE {{qual_table .SchemaName .TableName}}
|
||||||
|
ALTER COLUMN {{quote_ident .ColumnName}} TYPE {{.NewType}}{{if .UsingExpr}} USING {{.UsingExpr}}{{end}};
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
renamed_column := '{{.ColumnName}}_' || trim(both '_' from regexp_replace(lower(current_type), '[^a-z0-9]+', '_', 'g'));
|
||||||
|
EXECUTE format('ALTER TABLE {{qual_table .SchemaName .TableName}} RENAME COLUMN {{quote_ident .ColumnName}} TO %I', renamed_column);
|
||||||
|
ALTER TABLE {{qual_table .SchemaName .TableName}}
|
||||||
|
ADD COLUMN {{quote_ident .ColumnName}} {{.NewType}};
|
||||||
|
END;
|
||||||
END IF;
|
END IF;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1106,6 +1106,113 @@ 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")
|
||||||
|
|
||||||
|
table := models.InitTable("agent_skills", "public")
|
||||||
|
|
||||||
|
nameCol := models.InitColumn("name", "agent_skills", "public")
|
||||||
|
nameCol.Type = "integer"
|
||||||
|
table.Columns["name"] = nameCol
|
||||||
|
|
||||||
|
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, "EXCEPTION WHEN OTHERS THEN") {
|
||||||
|
t.Fatalf("expected guarded alter to fall back on conversion failure, got:\n%s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "renamed_column := 'name_' || trim(both '_' from regexp_replace(lower(current_type)") {
|
||||||
|
t.Fatalf("expected fallback to derive a renamed column name from the live type, got:\n%s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "RENAME COLUMN name TO %I") {
|
||||||
|
t.Fatalf("expected fallback to rename the existing column, got:\n%s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "ADD COLUMN name integer") {
|
||||||
|
t.Fatalf("expected fallback to add a fresh column with the new type, got:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user