From 1963f57b94abe4ca5849d788b5f4fa9392d185c3 Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
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 <lic@highgo.com>
---
 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; <waiting ...>
+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; <waiting ...>
+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)

