diff --git a/cmd/plan/plan.go b/cmd/plan/plan.go index df88a857..6f3855e5 100644 --- a/cmd/plan/plan.go +++ b/cmd/plan/plan.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "github.com/pgplex/pgschema/cmd/util" @@ -403,6 +404,12 @@ func processOutput(migrationPlan *plan.Plan, output outputSpec, cmd *cobra.Comma // and fail when applied to the target database. func normalizeSchemaNames(irData *ir.IR, fromSchema, toSchema string) { replaceString := newSchemaStringReplacer(fromSchema, toSchema) + // stripQualifiers removes same-schema function/type qualifiers from expressions. + // After replaceString converts temp schema references to toSchema, expressions may + // contain "toSchema.func_name(" or "::toSchema.type" which are redundant same-schema + // qualifiers. The initial normalizeIR (run by the inspector) couldn't strip these + // because it ran with the temp schema name, not the target schema. See issue #283. + stripQualifiers := newSameSchemaQualifierStripper(toSchema) // Normalize schema names in Schemas map if schema, exists := irData.Schemas[fromSchema]; exists { @@ -425,7 +432,7 @@ func normalizeSchemaNames(irData *ir.IR, fromSchema, toSchema string) { if constraint.ReferencedSchema == fromSchema { constraint.ReferencedSchema = toSchema } - constraint.CheckClause = replaceString(constraint.CheckClause) + constraint.CheckClause = stripQualifiers(replaceString(constraint.CheckClause)) } // Normalize schema references in table dependencies @@ -446,10 +453,10 @@ func normalizeSchemaNames(irData *ir.IR, fromSchema, toSchema string) { for _, column := range table.Columns { column.DataType = replaceString(column.DataType) if column.DefaultValue != nil { - *column.DefaultValue = replaceString(*column.DefaultValue) + *column.DefaultValue = stripQualifiers(replaceString(*column.DefaultValue)) } if column.GeneratedExpr != nil { - *column.GeneratedExpr = replaceString(*column.GeneratedExpr) + *column.GeneratedExpr = stripQualifiers(replaceString(*column.GeneratedExpr)) } } @@ -467,7 +474,7 @@ func normalizeSchemaNames(irData *ir.IR, fromSchema, toSchema string) { trigger.Schema = toSchema } trigger.Function = replaceString(trigger.Function) - trigger.Condition = replaceString(trigger.Condition) + trigger.Condition = stripQualifiers(replaceString(trigger.Condition)) } // Normalize schema names in RLS policies @@ -475,8 +482,8 @@ func normalizeSchemaNames(irData *ir.IR, fromSchema, toSchema string) { if policy.Schema == fromSchema { policy.Schema = toSchema } - policy.Using = replaceString(policy.Using) - policy.WithCheck = replaceString(policy.WithCheck) + policy.Using = stripQualifiers(replaceString(policy.Using)) + policy.WithCheck = stripQualifiers(replaceString(policy.WithCheck)) } } @@ -627,6 +634,28 @@ func newSchemaStringReplacer(fromSchema, toSchema string) func(string) string { } } +// newSameSchemaQualifierStripper creates a function that strips redundant same-schema +// qualifiers from SQL expressions. After normalizeSchemaNames replaces temp schema names +// with the target schema, expressions may contain "schema.func_name(" or "::schema.type" +// where the qualifier matches the object's own schema. These are redundant and must be +// stripped to match how the target database's inspector would produce them. See issue #283. +func newSameSchemaQualifierStripper(schema string) func(string) string { + if schema == "" { + return func(s string) string { return s } + } + prefix := schema + "." + funcPattern := regexp.MustCompile(regexp.QuoteMeta(prefix) + `([a-zA-Z_][a-zA-Z0-9_]*)\(`) + typePattern := regexp.MustCompile(`::` + regexp.QuoteMeta(prefix)) + return func(s string) string { + if s == "" || !strings.Contains(s, prefix) { + return s + } + s = funcPattern.ReplaceAllString(s, `${1}(`) + s = typePattern.ReplaceAllString(s, "::") + return s + } +} + // ResetFlags resets all global flag variables to their default values for testing func ResetFlags() { planHost = "localhost" diff --git a/testdata/diff/create_table/issue_283_function_default_schema_qualifier/diff.sql b/testdata/diff/create_table/issue_283_function_default_schema_qualifier/diff.sql new file mode 100644 index 00000000..e69de29b diff --git a/testdata/diff/create_table/issue_283_function_default_schema_qualifier/new.sql b/testdata/diff/create_table/issue_283_function_default_schema_qualifier/new.sql new file mode 100644 index 00000000..25ba03c8 --- /dev/null +++ b/testdata/diff/create_table/issue_283_function_default_schema_qualifier/new.sql @@ -0,0 +1,10 @@ +CREATE FUNCTION my_default_id() RETURNS uuid + LANGUAGE sql + AS $$ SELECT gen_random_uuid() $$; + +CREATE TABLE items ( + id uuid DEFAULT my_default_id() NOT NULL, + name text NOT NULL, + active boolean DEFAULT true NOT NULL, + CONSTRAINT items_pk PRIMARY KEY (id) +); diff --git a/testdata/diff/create_table/issue_283_function_default_schema_qualifier/old.sql b/testdata/diff/create_table/issue_283_function_default_schema_qualifier/old.sql new file mode 100644 index 00000000..25ba03c8 --- /dev/null +++ b/testdata/diff/create_table/issue_283_function_default_schema_qualifier/old.sql @@ -0,0 +1,10 @@ +CREATE FUNCTION my_default_id() RETURNS uuid + LANGUAGE sql + AS $$ SELECT gen_random_uuid() $$; + +CREATE TABLE items ( + id uuid DEFAULT my_default_id() NOT NULL, + name text NOT NULL, + active boolean DEFAULT true NOT NULL, + CONSTRAINT items_pk PRIMARY KEY (id) +); diff --git a/testdata/diff/create_table/issue_283_function_default_schema_qualifier/plan.json b/testdata/diff/create_table/issue_283_function_default_schema_qualifier/plan.json new file mode 100644 index 00000000..bdbe8800 --- /dev/null +++ b/testdata/diff/create_table/issue_283_function_default_schema_qualifier/plan.json @@ -0,0 +1,9 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.7.0", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "0915a8651243d1249ecffcf4938383a65f0a0fb644f189f372e3719f2a46f13f" + }, + "groups": null +} diff --git a/testdata/diff/create_table/issue_283_function_default_schema_qualifier/plan.sql b/testdata/diff/create_table/issue_283_function_default_schema_qualifier/plan.sql new file mode 100644 index 00000000..e69de29b diff --git a/testdata/diff/create_table/issue_283_function_default_schema_qualifier/plan.txt b/testdata/diff/create_table/issue_283_function_default_schema_qualifier/plan.txt new file mode 100644 index 00000000..241994af --- /dev/null +++ b/testdata/diff/create_table/issue_283_function_default_schema_qualifier/plan.txt @@ -0,0 +1 @@ +No changes detected. diff --git a/testdata/diff/dependency/table_fk_to_generated_column/plan.json b/testdata/diff/dependency/table_fk_to_generated_column/plan.json index 61bd1c44..a4b150ab 100644 --- a/testdata/diff/dependency/table_fk_to_generated_column/plan.json +++ b/testdata/diff/dependency/table_fk_to_generated_column/plan.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "pgschema_version": "1.6.2", + "pgschema_version": "1.7.0", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" @@ -21,7 +21,7 @@ "path": "public.calc_priority" }, { - "sql": "CREATE TABLE IF NOT EXISTS article (\n id integer,\n title text NOT NULL,\n priority integer GENERATED ALWAYS AS (public.calc_priority()) STORED,\n CONSTRAINT article_pkey PRIMARY KEY (id)\n);", + "sql": "CREATE TABLE IF NOT EXISTS article (\n id integer,\n title text NOT NULL,\n priority integer GENERATED ALWAYS AS (calc_priority()) STORED,\n CONSTRAINT article_pkey PRIMARY KEY (id)\n);", "type": "table", "operation": "create", "path": "public.article" diff --git a/testdata/diff/dependency/table_fk_to_generated_column/plan.sql b/testdata/diff/dependency/table_fk_to_generated_column/plan.sql index ab07d9d7..7df1404b 100644 --- a/testdata/diff/dependency/table_fk_to_generated_column/plan.sql +++ b/testdata/diff/dependency/table_fk_to_generated_column/plan.sql @@ -15,7 +15,7 @@ $$; CREATE TABLE IF NOT EXISTS article ( id integer, title text NOT NULL, - priority integer GENERATED ALWAYS AS (public.calc_priority()) STORED, + priority integer GENERATED ALWAYS AS (calc_priority()) STORED, CONSTRAINT article_pkey PRIMARY KEY (id) ); diff --git a/testdata/diff/dependency/table_fk_to_generated_column/plan.txt b/testdata/diff/dependency/table_fk_to_generated_column/plan.txt index b93f5d0d..9b704764 100644 --- a/testdata/diff/dependency/table_fk_to_generated_column/plan.txt +++ b/testdata/diff/dependency/table_fk_to_generated_column/plan.txt @@ -32,7 +32,7 @@ $$; CREATE TABLE IF NOT EXISTS article ( id integer, title text NOT NULL, - priority integer GENERATED ALWAYS AS (public.calc_priority()) STORED, + priority integer GENERATED ALWAYS AS (calc_priority()) STORED, CONSTRAINT article_pkey PRIMARY KEY (id) );