Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 24 additions & 26 deletions internal/diff/column.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,36 +112,34 @@ func (cd *ColumnDiff) generateColumnSQL(tableSchema, tableName string, targetSch

// needsUsingClause determines if a type conversion requires a USING clause.
//
// This is especially important when converting to or from custom types (like ENUMs),
// because PostgreSQL often cannot implicitly cast these types. To avoid generating
// invalid migrations, this function takes a conservative approach:
//
// - Any conversion involving at least one non–built-in (custom) type will require
// a USING clause.
// - For built-in → built-in conversions we still assume PostgreSQL provides an
// implicit cast in most cases; callers should be aware that some edge cases
// (e.g. certain text → json conversions) may still need manual adjustment.
// PostgreSQL rejects bare ALTER COLUMN TYPE when no implicit cast exists between
// the old and new types (SQLSTATE 42804). When the base type names differ, we
// always emit USING col::newtype — a redundant USING is harmless when an implicit
// cast exists, but a missing one breaks the migration. When only modifiers change
// (e.g. numeric(18,6) → numeric(20,6)), no USING is needed.
func needsUsingClause(oldType, newType string) bool {
// Check if old type is text-like
oldIsTextLike := ir.IsTextLikeType(oldType)

// Determine whether the old/new types are PostgreSQL built-ins
oldIsBuiltIn := ir.IsBuiltInType(oldType)
newIsBuiltIn := ir.IsBuiltInType(newType)

// Preserve existing behavior: text-like → non–built-in likely needs USING
if oldIsTextLike && !newIsBuiltIn {
return true
oldNorm := normalizeBaseTypeName(oldType)
newNorm := normalizeBaseTypeName(newType)
if oldNorm == newNorm {
return false
}
return true
}

// Be conservative for any conversion involving custom (non–built-in) types:
// this covers custom → custom and built-in ↔ custom conversions.
if !oldIsBuiltIn || !newIsBuiltIn {
return true
func normalizeBaseTypeName(typeName string) string {
t := typeName
if open := strings.Index(t, "("); open != -1 {
if close := strings.Index(t[open:], ")"); close != -1 {
t = t[:open] + t[open+close+1:]
} else {
t = t[:open]
}
}
Comment thread
tianzhou marked this conversation as resolved.

// For built-in → built-in types we assume an implicit cast is available.
return false
t = strings.TrimPrefix(t, "pg_catalog.")
if !strings.Contains(t, "\"") {
t = strings.ToLower(t)
}
return t
Comment thread
tianzhou marked this conversation as resolved.
}

// comparableColumnType returns the column's data type including any
Expand Down
59 changes: 59 additions & 0 deletions internal/diff/column_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package diff

import "testing"

func TestNormalizeBaseTypeName(t *testing.T) {
tests := []struct {
input string
want string
}{
{"integer", "integer"},
{"INTEGER", "integer"},
{"bigint", "bigint"},
{"numeric(18,6)", "numeric"},
{"numeric(20,6)", "numeric"},
{"varchar(128)", "varchar"},
{"character varying(255)", "character varying"},
{"integer[]", "integer[]"},
{"character varying(128)[]", "character varying[]"},
{"timestamp(6) with time zone", "timestamp with time zone"},
{"pg_catalog.int4", "int4"},
{`"MyStatus"`, `"MyStatus"`},
{`"mystatus"`, `"mystatus"`},
{`"MyStatus"[]`, `"MyStatus"[]`},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := normalizeBaseTypeName(tt.input)
if got != tt.want {
t.Errorf("normalizeBaseTypeName(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}

func TestNeedsUsingClause(t *testing.T) {
tests := []struct {
old string
new string
want bool
}{
{"text", "integer", true},
{"integer", "bigint", true},
{"numeric(18,6)", "numeric(20,6)", false},
{"integer", "integer[]", true},
{"text", "action_type", true},
{"varchar(128)", "varchar(255)", false},
{"timestamp(3) with time zone", "timestamp(6) with time zone", false},
{"timestamp without time zone", "timestamp with time zone", true},
{`"MyStatus"`, `"mystatus"`, true},
}
for _, tt := range tests {
t.Run(tt.old+"→"+tt.new, func(t *testing.T) {
got := needsUsingClause(tt.old, tt.new)
if got != tt.want {
t.Errorf("needsUsingClause(%q, %q) = %v, want %v", tt.old, tt.new, got, tt.want)
}
})
}
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ALTER TABLE ex ALTER COLUMN "ID" TYPE bigint;
ALTER TABLE ex ALTER COLUMN "ID" TYPE bigint USING "ID"::bigint;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "1.0.0",
"pgschema_version": "1.12.1",
"pgschema_version": "1.12.2",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "9d443bc536153eed8fce077bfacc3d7f42b1a94f02d33868bb78be3b9de05088"
Expand All @@ -9,7 +9,7 @@
{
"steps": [
{
"sql": "ALTER TABLE ex ALTER COLUMN \"ID\" TYPE bigint;",
"sql": "ALTER TABLE ex ALTER COLUMN \"ID\" TYPE bigint USING \"ID\"::bigint;",
"type": "table.column",
"operation": "alter",
"path": "public.ex.ID"
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ALTER TABLE ex ALTER COLUMN "ID" TYPE bigint;
ALTER TABLE ex ALTER COLUMN "ID" TYPE bigint USING "ID"::bigint;
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ Tables:
DDL to be executed:
--------------------------------------------------

ALTER TABLE ex ALTER COLUMN "ID" TYPE bigint;
ALTER TABLE ex ALTER COLUMN "ID" TYPE bigint USING "ID"::bigint;
14 changes: 11 additions & 3 deletions testdata/diff/create_table/alter_column_types/diff.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ CREATE TYPE action_type AS ENUM (
'rejected'
);

ALTER TABLE user_pending_permissions ALTER COLUMN id TYPE bigint;
ALTER TABLE user_pending_permissions ALTER COLUMN id TYPE bigint USING id::bigint;

ALTER TABLE user_pending_permissions ALTER COLUMN user_id TYPE bigint;
ALTER TABLE user_pending_permissions ALTER COLUMN user_id TYPE bigint USING user_id::bigint;

ALTER TABLE user_pending_permissions ALTER COLUMN object_ids_ints TYPE bigint[];
ALTER TABLE user_pending_permissions ALTER COLUMN object_ids_ints TYPE bigint[] USING object_ids_ints::bigint[];

ALTER TABLE user_pending_permissions ALTER COLUMN action TYPE action_type USING action::action_type;

Expand All @@ -21,3 +21,11 @@ ALTER TABLE user_pending_permissions ALTER COLUMN status SET DEFAULT 'pending'::
ALTER TABLE user_pending_permissions ALTER COLUMN tags TYPE action_type[] USING tags::action_type[];

ALTER TABLE user_pending_permissions ALTER COLUMN amount TYPE numeric(20,6);

ALTER TABLE user_pending_permissions ALTER COLUMN arfcn_dl DROP DEFAULT;

ALTER TABLE user_pending_permissions ALTER COLUMN arfcn_dl TYPE integer USING arfcn_dl::integer;

ALTER TABLE user_pending_permissions ALTER COLUMN arfcn_dl SET DEFAULT 0;

ALTER TABLE user_pending_permissions ALTER COLUMN priority TYPE integer USING priority::integer;
4 changes: 3 additions & 1 deletion testdata/diff/create_table/alter_column_types/new.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ CREATE TABLE public.user_pending_permissions (
action public.action_type,
status public.action_type DEFAULT 'pending',
tags public.action_type[],
amount numeric(20,6) NOT NULL DEFAULT 0
amount numeric(20,6) NOT NULL DEFAULT 0,
arfcn_dl integer DEFAULT 0,
priority integer
);
4 changes: 3 additions & 1 deletion testdata/diff/create_table/alter_column_types/old.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ CREATE TABLE public.user_pending_permissions (
action text,
status text DEFAULT 'pending',
tags text[],
amount numeric(18,6) NOT NULL DEFAULT 0
amount numeric(18,6) NOT NULL DEFAULT 0,
arfcn_dl text DEFAULT 'unknown',
priority text
);
34 changes: 29 additions & 5 deletions testdata/diff/create_table/alter_column_types/plan.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"version": "1.0.0",
"pgschema_version": "1.12.1",
"pgschema_version": "1.12.2",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "614655f95d349ec321da570d4e232db71f8bfb57404998153f9fd53720cd2acb"
"hash": "31ef5d21f4bfa9713df469db3eb54585bf60ea348f09608b21f7d05eae9f3f02"
},
"groups": [
{
Expand All @@ -15,19 +15,19 @@
"path": "public.action_type"
},
{
"sql": "ALTER TABLE user_pending_permissions ALTER COLUMN id TYPE bigint;",
"sql": "ALTER TABLE user_pending_permissions ALTER COLUMN id TYPE bigint USING id::bigint;",
"type": "table.column",
"operation": "alter",
"path": "public.user_pending_permissions.id"
},
{
"sql": "ALTER TABLE user_pending_permissions ALTER COLUMN user_id TYPE bigint;",
"sql": "ALTER TABLE user_pending_permissions ALTER COLUMN user_id TYPE bigint USING user_id::bigint;",
"type": "table.column",
"operation": "alter",
"path": "public.user_pending_permissions.user_id"
},
{
"sql": "ALTER TABLE user_pending_permissions ALTER COLUMN object_ids_ints TYPE bigint[];",
"sql": "ALTER TABLE user_pending_permissions ALTER COLUMN object_ids_ints TYPE bigint[] USING object_ids_ints::bigint[];",
"type": "table.column",
"operation": "alter",
"path": "public.user_pending_permissions.object_ids_ints"
Expand Down Expand Up @@ -67,6 +67,30 @@
"type": "table.column",
"operation": "alter",
"path": "public.user_pending_permissions.amount"
},
{
"sql": "ALTER TABLE user_pending_permissions ALTER COLUMN arfcn_dl DROP DEFAULT;",
"type": "table.column",
"operation": "alter",
"path": "public.user_pending_permissions.arfcn_dl"
},
{
"sql": "ALTER TABLE user_pending_permissions ALTER COLUMN arfcn_dl TYPE integer USING arfcn_dl::integer;",
"type": "table.column",
"operation": "alter",
"path": "public.user_pending_permissions.arfcn_dl"
},
{
"sql": "ALTER TABLE user_pending_permissions ALTER COLUMN arfcn_dl SET DEFAULT 0;",
"type": "table.column",
"operation": "alter",
"path": "public.user_pending_permissions.arfcn_dl"
},
{
"sql": "ALTER TABLE user_pending_permissions ALTER COLUMN priority TYPE integer USING priority::integer;",
"type": "table.column",
"operation": "alter",
"path": "public.user_pending_permissions.priority"
}
]
}
Expand Down
14 changes: 11 additions & 3 deletions testdata/diff/create_table/alter_column_types/plan.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ CREATE TYPE action_type AS ENUM (
'rejected'
);

ALTER TABLE user_pending_permissions ALTER COLUMN id TYPE bigint;
ALTER TABLE user_pending_permissions ALTER COLUMN id TYPE bigint USING id::bigint;

ALTER TABLE user_pending_permissions ALTER COLUMN user_id TYPE bigint;
ALTER TABLE user_pending_permissions ALTER COLUMN user_id TYPE bigint USING user_id::bigint;

ALTER TABLE user_pending_permissions ALTER COLUMN object_ids_ints TYPE bigint[];
ALTER TABLE user_pending_permissions ALTER COLUMN object_ids_ints TYPE bigint[] USING object_ids_ints::bigint[];

ALTER TABLE user_pending_permissions ALTER COLUMN action TYPE action_type USING action::action_type;

Expand All @@ -21,3 +21,11 @@ ALTER TABLE user_pending_permissions ALTER COLUMN status SET DEFAULT 'pending'::
ALTER TABLE user_pending_permissions ALTER COLUMN tags TYPE action_type[] USING tags::action_type[];

ALTER TABLE user_pending_permissions ALTER COLUMN amount TYPE numeric(20,6);

ALTER TABLE user_pending_permissions ALTER COLUMN arfcn_dl DROP DEFAULT;

ALTER TABLE user_pending_permissions ALTER COLUMN arfcn_dl TYPE integer USING arfcn_dl::integer;

ALTER TABLE user_pending_permissions ALTER COLUMN arfcn_dl SET DEFAULT 0;

ALTER TABLE user_pending_permissions ALTER COLUMN priority TYPE integer USING priority::integer;
16 changes: 13 additions & 3 deletions testdata/diff/create_table/alter_column_types/plan.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ Tables:
~ user_pending_permissions
~ action (column)
~ amount (column)
~ arfcn_dl (column)
~ id (column)
~ object_ids_ints (column)
~ priority (column)
~ status (column)
~ tags (column)
~ user_id (column)
Expand All @@ -26,11 +28,11 @@ CREATE TYPE action_type AS ENUM (
'rejected'
);

ALTER TABLE user_pending_permissions ALTER COLUMN id TYPE bigint;
ALTER TABLE user_pending_permissions ALTER COLUMN id TYPE bigint USING id::bigint;

ALTER TABLE user_pending_permissions ALTER COLUMN user_id TYPE bigint;
ALTER TABLE user_pending_permissions ALTER COLUMN user_id TYPE bigint USING user_id::bigint;

ALTER TABLE user_pending_permissions ALTER COLUMN object_ids_ints TYPE bigint[];
ALTER TABLE user_pending_permissions ALTER COLUMN object_ids_ints TYPE bigint[] USING object_ids_ints::bigint[];

ALTER TABLE user_pending_permissions ALTER COLUMN action TYPE action_type USING action::action_type;

Expand All @@ -43,3 +45,11 @@ ALTER TABLE user_pending_permissions ALTER COLUMN status SET DEFAULT 'pending'::
ALTER TABLE user_pending_permissions ALTER COLUMN tags TYPE action_type[] USING tags::action_type[];

ALTER TABLE user_pending_permissions ALTER COLUMN amount TYPE numeric(20,6);

ALTER TABLE user_pending_permissions ALTER COLUMN arfcn_dl DROP DEFAULT;

ALTER TABLE user_pending_permissions ALTER COLUMN arfcn_dl TYPE integer USING arfcn_dl::integer;

ALTER TABLE user_pending_permissions ALTER COLUMN arfcn_dl SET DEFAULT 0;

ALTER TABLE user_pending_permissions ALTER COLUMN priority TYPE integer USING priority::integer;