From 1963f57b94abe4ca5849d788b5f4fa9392d185c3 Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Mon, 8 Jun 2026 11:07:15 +0800 Subject: [PATCH v1] Fix ALTER DOMAIN VALIDATE CONSTRAINT locking ALTER DOMAIN ... VALIDATE CONSTRAINT must wait for already-running DML commands on tables using the domain. Those commands may have initialized domain constraint checks before a NOT VALID constraint was added, so they can still insert or update rows that violate the new constraint. Commit 16a0039dc reduced the related-relation lock level to ShareUpdateExclusiveLock, relying on new rows being checked against NOT VALID constraints. That is true for DML started after the constraint is added, but not for DML that was already running. Use ShareLock during validation again, so validation cannot mark the constraint valid before such stale DML finishes. Add an isolation test for the race. Author: Chao Li --- src/backend/commands/typecmds.c | 9 +++--- .../expected/alter-domain-validate.out | 17 +++++++++++ src/test/isolation/isolation_schedule | 1 + .../specs/alter-domain-validate.spec | 29 +++++++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 src/test/isolation/expected/alter-domain-validate.out create mode 100644 src/test/isolation/specs/alter-domain-validate.spec diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index c4c3cdb5461..f1a6190608e 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -3136,11 +3136,12 @@ AlterDomainValidateConstraint(List *names, const char *constrName) conbin = TextDatumGetCString(val); /* - * Locking related relations with ShareUpdateExclusiveLock is ok - * because not-yet-valid constraints are still enforced against - * concurrent inserts or updates. + * Use ShareLock here to wait for already-running commands that can + * insert or update values in tables using this domain. Such commands + * may have initialized domain constraint checks before this NOT VALID + * constraint was added. */ - validateDomainCheckConstraint(domainoid, conbin, ShareUpdateExclusiveLock); + validateDomainCheckConstraint(domainoid, conbin, ShareLock); /* * Now update the catalog, while we have the door open. diff --git a/src/test/isolation/expected/alter-domain-validate.out b/src/test/isolation/expected/alter-domain-validate.out new file mode 100644 index 00000000000..69b18048292 --- /dev/null +++ b/src/test/isolation/expected/alter-domain-validate.out @@ -0,0 +1,17 @@ +Parsed test spec with 3 sessions + +starting permutation: s1_lock s2_insert s3_add s3_validate s1_unlock s3_check +step s1_lock: DO $$ BEGIN PERFORM pg_advisory_lock(8888); END $$; +step s2_insert: WITH wait AS MATERIALIZED (SELECT pg_advisory_lock(8888)) INSERT INTO alter_domain_validate_t SELECT (-1)::alter_domain_validate_d FROM wait; +step s3_add: ALTER DOMAIN alter_domain_validate_d ADD CONSTRAINT alter_domain_validate_d_pos CHECK (VALUE > 0) NOT VALID; +step s3_validate: ALTER DOMAIN alter_domain_validate_d VALIDATE CONSTRAINT alter_domain_validate_d_pos; +step s1_unlock: DO $$ BEGIN PERFORM pg_advisory_unlock(8888); END $$; +step s2_insert: <... completed> +step s3_validate: <... completed> +ERROR: column "a" of table "alter_domain_validate_t" contains values that violate the new constraint +step s3_check: SELECT count(*) FROM alter_domain_validate_t; +count +----- + 1 +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 15c33fad4c5..b8ebe92553c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -90,6 +90,7 @@ test: skip-locked-3 test: skip-locked-4 test: drop-index-concurrently-1 test: multiple-cic +test: alter-domain-validate test: alter-table-1 test: alter-table-2 test: alter-table-3 diff --git a/src/test/isolation/specs/alter-domain-validate.spec b/src/test/isolation/specs/alter-domain-validate.spec new file mode 100644 index 00000000000..f84c7685e93 --- /dev/null +++ b/src/test/isolation/specs/alter-domain-validate.spec @@ -0,0 +1,29 @@ +# Test ALTER DOMAIN VALIDATE CONSTRAINT waits for already-running DML. + +setup +{ + CREATE DOMAIN alter_domain_validate_d AS int; + CREATE TABLE alter_domain_validate_t (a alter_domain_validate_d); +} + +teardown +{ + DROP TABLE alter_domain_validate_t; + DROP DOMAIN alter_domain_validate_d; +} + +session s1 +step s1_lock { DO $$ BEGIN PERFORM pg_advisory_lock(8888); END $$; } +step s1_unlock { DO $$ BEGIN PERFORM pg_advisory_unlock(8888); END $$; } + +session s2 +# CoerceToDomain initializes the domain constraint list during executor +# startup, before this CTE waits on the advisory lock. +step s2_insert { WITH wait AS MATERIALIZED (SELECT pg_advisory_lock(8888)) INSERT INTO alter_domain_validate_t SELECT (-1)::alter_domain_validate_d FROM wait; } + +session s3 +step s3_add { ALTER DOMAIN alter_domain_validate_d ADD CONSTRAINT alter_domain_validate_d_pos CHECK (VALUE > 0) NOT VALID; } +step s3_validate { ALTER DOMAIN alter_domain_validate_d VALIDATE CONSTRAINT alter_domain_validate_d_pos; } +step s3_check { SELECT count(*) FROM alter_domain_validate_t; } + +permutation s1_lock s2_insert s3_add s3_validate s1_unlock s3_check -- 2.50.1 (Apple Git-155)