Skip to content

pg-delta: domain CHECK constraint dependents are silently skipped by replacement expansion, producing plans that fail at apply #286

Description

@avallete

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.

  • 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

CREATE SCHEMA probe_domain_check;
CREATE FUNCTION probe_domain_check.is_valid(value text)
RETURNS boolean
LANGUAGE sql
IMMUTABLE
AS $$ SELECT length(value) > 0 $$;

CREATE DOMAIN probe_domain_check.code AS text
  CONSTRAINT code_is_valid CHECK (probe_domain_check.is_valid(VALUE));

Target state

ALTER DOMAIN probe_domain_check.code DROP CONSTRAINT code_is_valid;
DROP FUNCTION probe_domain_check.is_valid(text);
CREATE FUNCTION probe_domain_check.is_valid(input text)
RETURNS boolean
LANGUAGE sql
IMMUTABLE
AS $$ SELECT length(input) > 0 $$;
ALTER DOMAIN probe_domain_check.code
  ADD CONSTRAINT code_is_valid CHECK (probe_domain_check.is_valid(VALUE));

Actual generated plan

SET check_function_bodies = false;
DROP FUNCTION probe_domain_check.is_valid(value text);
CREATE FUNCTION probe_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

Expected plan shape

ALTER DOMAIN probe_domain_check.code DROP CONSTRAINT code_is_valid;
DROP FUNCTION probe_domain_check.is_valid(value text);
CREATE FUNCTION probe_domain_check.is_valid(input text) ...;
ALTER DOMAIN probe_domain_check.code ADD CONSTRAINT code_is_valid CHECK (probe_domain_check.is_valid(VALUE));

Suggested fix direction

  • 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.
  • Coordinate with the pg-delta: function replacement can over-recreate tables for default and check-constraint dependencies #280 fix, which restructures the same column:*/constraint:* promotion logic; this case should be covered (or at least not regressed) by whatever shape that fix takes.

Refs #280.

Metadata

Metadata

Assignees

No one assigned

    Labels

    supabase/pg-toolbelt🐛 BugDefects, regressions, or unintended behavior that need fixing

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions