You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while reviewing #280. Same code path (expandReplaceDependencies / normalizeDependentId), but the opposite failure mode: instead of over-recreating the owning object, domain CHECK constraint dependents are dropped from the replacement walk entirely, so the generated plan fails at apply time.
Affected package: pg-delta
Tested commit: e396579 (current main)
PostgreSQL version: 17.6 via integration container
Root cause
normalizeDependentId in src/core/expand-replace-dependencies.ts rewrites every constraint:schema.X.name dependent to table:schema.X. But depend.ts also produces constraint: stable ids for domain constraints (constraint:schema.typname.conname, "Constraints on domain" branch). Those get promoted to a nonexistent table:schema.typname, resolveObjectForStableId returns null, and the dependent is silently skipped — no targeted change, no replacement, nothing.
This is also why the domain CHECK "boundary check" probe reported in #280 appeared to pass: it passes by accident, only when the regular diff independently emits targeted constraint changes (because the constraint expression text changed). When the constraint expression is textually unchanged across the replacement, the diff emits nothing, the expansion skips the dependent, and the plan breaks.
Reproduction
A parameter rename is signature-breaking for CREATE OR REPLACE (see SIGNATURE_BREAKING_FIELDS in procedure.diff.ts), so it forces DROP FUNCTION + CREATE FUNCTION with the same stable id — and leaves the domain constraint expression textually identical.
Source state
CREATESCHEMAprobe_domain_check;
CREATEFUNCTIONprobe_domain_check.is_valid(value text)
RETURNS boolean
LANGUAGE sql
IMMUTABLE
AS $$ SELECT length(value) >0 $$;
CREATEDOMAINprobe_domain_check.code AStextCONSTRAINT code_is_valid CHECK (probe_domain_check.is_valid(VALUE));
Target state
ALTERDOMAINprobe_domain_check.code DROP CONSTRAINT code_is_valid;
DROPFUNCTIONprobe_domain_check.is_valid(text);
CREATEFUNCTIONprobe_domain_check.is_valid(input text)
RETURNS boolean
LANGUAGE sql
IMMUTABLE
AS $$ SELECT length(input) >0 $$;
ALTERDOMAINprobe_domain_check.code
ADD CONSTRAINT code_is_valid CHECK (probe_domain_check.is_valid(VALUE));
Actual generated plan
SET check_function_bodies = false;
DROPFUNCTIONprobe_domain_check.is_valid(value text);
CREATEFUNCTIONprobe_domain_check.is_valid(input text)
RETURNS boolean
LANGUAGE sql
IMMUTABLE
AS $function$ SELECT length(input) >0 $function$;
No handling of the domain constraint at all. Applying the plan fails on the first statement:
ERROR: cannot drop function probe_domain_check.is_valid(text) because other objects depend on it
Make the constraint: handling in normalizeDependentId domain-aware: a constraint:schema.X.name id can be owned by a table or a domain, and the two need different targeted treatments (ALTER TABLE ... DROP/ADD CONSTRAINT vs ALTER DOMAIN ... DROP/ADD CONSTRAINT). Resolving against the catalogs (does table:schema.X exist? does domain:schema.X?) disambiguates without parsing.
The expansion must synthesize the targeted drop/add pair itself in this case — the regular diff emits nothing because the constraint is unchanged between the two databases.
Bug report
Found while reviewing #280. Same code path (
expandReplaceDependencies/normalizeDependentId), but the opposite failure mode: instead of over-recreating the owning object, domain CHECK constraint dependents are dropped from the replacement walk entirely, so the generated plan fails at apply time.pg-deltae396579(currentmain)Root cause
normalizeDependentIdinsrc/core/expand-replace-dependencies.tsrewrites everyconstraint:schema.X.namedependent totable:schema.X. Butdepend.tsalso producesconstraint:stable ids for domain constraints (constraint:schema.typname.conname, "Constraints on domain" branch). Those get promoted to a nonexistenttable:schema.typname,resolveObjectForStableIdreturnsnull, and the dependent is silently skipped — no targeted change, no replacement, nothing.This is also why the domain CHECK "boundary check" probe reported in #280 appeared to pass: it passes by accident, only when the regular diff independently emits targeted constraint changes (because the constraint expression text changed). When the constraint expression is textually unchanged across the replacement, the diff emits nothing, the expansion skips the dependent, and the plan breaks.
Reproduction
A parameter rename is signature-breaking for
CREATE OR REPLACE(seeSIGNATURE_BREAKING_FIELDSinprocedure.diff.ts), so it forcesDROP FUNCTION+CREATE FUNCTIONwith the same stable id — and leaves the domain constraint expression textually identical.Source state
Target state
Actual generated plan
No handling of the domain constraint at all. Applying the plan fails on the first statement:
Expected plan shape
Suggested fix direction
constraint:handling innormalizeDependentIddomain-aware: aconstraint:schema.X.nameid can be owned by a table or a domain, and the two need different targeted treatments (ALTER TABLE ... DROP/ADD CONSTRAINTvsALTER DOMAIN ... DROP/ADD CONSTRAINT). Resolving against the catalogs (doestable:schema.Xexist? doesdomain:schema.X?) disambiguates without parsing.column:*/constraint:*promotion logic; this case should be covered (or at least not regressed) by whatever shape that fix takes.Refs #280.