From 6e93dd2a710b13b8eff723414b4ce82622f3ed99 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 16 Mar 2026 12:04:26 +0800
Subject: [PATCH v1 1/1] table rewrite for container type include constrained
 domain

CREATE DOMAIN domain7 as int CHECK(random(min=>1::int, max=>11) < value);
CREATE TABLE test_add_domain_col(a int);
ALTER TABLE test_add_domain_col ADD COLUMN x domain7[] default '{5}';

The above ALTER TABLE ADD COLUMN should do table rewrite.

discussion: https://postgr.es/m/
commitfest: https://commitfest.postgresql.org/patch/
---
 src/backend/commands/tablecmds.c           | 103 ++++++++++++++++++++-
 src/test/regress/expected/fast_default.out |  28 ++++++
 src/test/regress/sql/fast_default.sql      |  23 +++++
 3 files changed, 151 insertions(+), 3 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a2e3b72f156..33c5a4f9000 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -503,6 +503,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
 									 bool recurse, bool recursing,
 									 LOCKMODE lockmode, AlterTablePass cur_pass,
 									 AlterTableUtilityContext *context);
+static void type_has_constrained_domain(Oid typoid, bool *has_constr,
+										bool *has_volatile);
 static bool check_for_column_name_collision(Relation rel, const char *colname,
 											bool if_not_exists);
 static void add_column_datatype_dependency(Oid relid, int32 attnum, Oid typid);
@@ -7506,7 +7508,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 */
 	if (RELKIND_HAS_STORAGE(relkind))
 	{
-		bool		has_domain_constraints;
+		bool		has_domain_constraints = false;
 		bool		has_missing = false;
 		bool		has_volatile = false;
 
@@ -7526,8 +7528,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		else
 			defval = (Expr *) build_column_default(rel, attribute->attnum);
 
-		has_domain_constraints =
-			DomainHasConstraints(attribute->atttypid, &has_volatile);
+		type_has_constrained_domain(attribute->atttypid,
+									&has_domain_constraints,
+									&has_volatile);
 
 		/*
 		 * If the domain has volatile constraints, we must do a table rewrite
@@ -7714,6 +7717,100 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	return address;
 }
 
+ /*
+  * Checks whether a type contains a constrained domain. This recurses into
+  * container types (arrays, composites, ranges, multiranges) to examine their
+  * element or subtypes.
+  *
+  * The caller must initialize *has_constr to false before calling. If a
+  * constrained domain is found at any level, has_constr is set to true.
+  * Obviously, has_constr will set to true for simple constrained domain.
+  *
+  * has_volatile is set to true if the element/subtypes type is a domain and
+  * that domain contains a volatile expression.
+  */
+static void
+type_has_constrained_domain(Oid typoid, bool *has_constr, bool *has_volatile)
+{
+	char		att_typtype = get_typtype(typoid);
+
+	/* since this function recurses, it could be driven to stack overflow */
+	check_stack_depth();
+
+	Assert(has_constr != NULL);
+
+	if (has_volatile)
+	{
+		if (*has_volatile)
+			return;
+	}
+	else if (*has_constr)
+		return;
+
+	if (att_typtype == TYPTYPE_DOMAIN)
+	{
+		*has_constr = DomainHasConstraints(typoid, has_volatile);
+		return;
+	}
+	else if (att_typtype == TYPTYPE_COMPOSITE)
+	{
+		/*
+		 * For a composite type, recurse into its attributes.  Use the
+		 * typcache to avoid opening the relation directly.
+		 */
+		TupleDesc	tupdesc = lookup_rowtype_tupdesc(typoid, -1);
+
+		for (int i = 0; i < tupdesc->natts; i++)
+		{
+			Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+
+			if (attr->attisdropped)
+				continue;
+
+			type_has_constrained_domain(attr->atttypid,
+										has_constr,
+										has_volatile);
+
+			if (has_volatile)
+			{
+				if (*has_volatile)
+					break;
+			}
+			else if (*has_constr)
+				break;
+		}
+		ReleaseTupleDesc(tupdesc);
+		return;
+	}
+	else if (att_typtype == TYPTYPE_RANGE)
+	{
+		type_has_constrained_domain(get_range_subtype(typoid),
+									has_constr,
+									has_volatile);
+		return;
+	}
+	else if (att_typtype == TYPTYPE_MULTIRANGE)
+	{
+		type_has_constrained_domain(get_multirange_range(typoid),
+									has_constr,
+									has_volatile);
+		return;
+	}
+	else
+	{
+		Oid			att_typelem = get_element_type(typoid);
+
+		if (OidIsValid(att_typelem))
+		{
+			/* recurse into array element type */
+			type_has_constrained_domain(att_typelem,
+										has_constr,
+										has_volatile);
+			return;
+		}
+	}
+}
+
 /*
  * If a new or renamed column will collide with the name of an existing
  * column and if_not_exists is false then error out, else do nothing.
diff --git a/src/test/regress/expected/fast_default.out b/src/test/regress/expected/fast_default.out
index ffbc47089b1..16339d6d209 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -372,8 +372,36 @@ NOTICE:  rewriting table test_add_domain_col for reason 2
 ALTER TABLE test_add_domain_col ADD COLUMN h domain5
   GENERATED ALWAYS AS (a + 20) VIRTUAL;  -- error
 ERROR:  virtual generated column "h" cannot have a domain type
+-- Test container type contains volatile domain for table rewrite
+CREATE TYPE domain6range AS range(subtype=domain6);
+CREATE TYPE comp_with_domain6 AS (a domain6);
+CREATE TYPE domain7range AS range(subtype=domain7);
+CREATE TYPE comp_with_domain7 AS (a domain7);
+-- all of the following should have table rewrite
+ALTER TABLE test_add_domain_col ADD COLUMN x domain7[] DEFAULT '{20}';
+NOTICE:  rewriting table test_add_domain_col for reason 2
+ALTER TABLE test_add_domain_col ADD COLUMN x1 domain7[];
+NOTICE:  rewriting table test_add_domain_col for reason 2
+ALTER TABLE test_add_domain_col ADD COLUMN x2 domain7range DEFAULT '[12, 22)';
+NOTICE:  rewriting table test_add_domain_col for reason 2
+ALTER TABLE test_add_domain_col ADD COLUMN x3 domain7range[] DEFAULT '{"[12,22)"}';
+NOTICE:  rewriting table test_add_domain_col for reason 2
+ALTER TABLE test_add_domain_col ADD COLUMN x4 domain7multirange DEFAULT '{[12, 22]}';
+NOTICE:  rewriting table test_add_domain_col for reason 2
+ALTER TABLE test_add_domain_col ADD COLUMN x5 domain7multirange[] DEFAULT '{"{[12,22]}"}';
+NOTICE:  rewriting table test_add_domain_col for reason 2
+ALTER TABLE test_add_domain_col ADD COLUMN x6 comp_with_domain7 DEFAULT '(12)';
+NOTICE:  rewriting table test_add_domain_col for reason 2
+ALTER TABLE test_add_domain_col ADD COLUMN x7 comp_with_domain7[] DEFAULT '{(12)}';
+NOTICE:  rewriting table test_add_domain_col for reason 2
+-- all of the above should have table rewrite
+-- No need table rewrite, as domain volatile default expressions are irrelevant for container types
+ALTER TABLE test_add_domain_col ADD COLUMN x10 comp_with_domain6 DEFAULT '(12)';
+ALTER TABLE test_add_domain_col ADD COLUMN x11 comp_with_domain6[] DEFAULT '{(12)}';
+ALTER TABLE test_add_domain_col ADD COLUMN x12 domain8[] DEFAULT '{12}';
 DROP TABLE t2;
 DROP TABLE test_add_domain_col;
+DROP TYPE domain7range, comp_with_domain7, comp_with_domain6, domain6range;
 DROP DOMAIN domain1;
 DROP DOMAIN domain2;
 DROP DOMAIN domain3;
diff --git a/src/test/regress/sql/fast_default.sql b/src/test/regress/sql/fast_default.sql
index 8ff29cf2697..0bf11ed786c 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -332,8 +332,31 @@ ALTER TABLE test_add_domain_col ADD COLUMN g domain9 DEFAULT 14;
 ALTER TABLE test_add_domain_col ADD COLUMN h domain5
   GENERATED ALWAYS AS (a + 20) VIRTUAL;  -- error
 
+-- Test container type contains volatile domain for table rewrite
+CREATE TYPE domain6range AS range(subtype=domain6);
+CREATE TYPE comp_with_domain6 AS (a domain6);
+CREATE TYPE domain7range AS range(subtype=domain7);
+CREATE TYPE comp_with_domain7 AS (a domain7);
+
+-- all of the following should have table rewrite
+ALTER TABLE test_add_domain_col ADD COLUMN x domain7[] DEFAULT '{20}';
+ALTER TABLE test_add_domain_col ADD COLUMN x1 domain7[];
+ALTER TABLE test_add_domain_col ADD COLUMN x2 domain7range DEFAULT '[12, 22)';
+ALTER TABLE test_add_domain_col ADD COLUMN x3 domain7range[] DEFAULT '{"[12,22)"}';
+ALTER TABLE test_add_domain_col ADD COLUMN x4 domain7multirange DEFAULT '{[12, 22]}';
+ALTER TABLE test_add_domain_col ADD COLUMN x5 domain7multirange[] DEFAULT '{"{[12,22]}"}';
+ALTER TABLE test_add_domain_col ADD COLUMN x6 comp_with_domain7 DEFAULT '(12)';
+ALTER TABLE test_add_domain_col ADD COLUMN x7 comp_with_domain7[] DEFAULT '{(12)}';
+-- all of the above should have table rewrite
+
+-- No need table rewrite, as domain volatile default expressions are irrelevant for container types
+ALTER TABLE test_add_domain_col ADD COLUMN x10 comp_with_domain6 DEFAULT '(12)';
+ALTER TABLE test_add_domain_col ADD COLUMN x11 comp_with_domain6[] DEFAULT '{(12)}';
+ALTER TABLE test_add_domain_col ADD COLUMN x12 domain8[] DEFAULT '{12}';
+
 DROP TABLE t2;
 DROP TABLE test_add_domain_col;
+DROP TYPE domain7range, comp_with_domain7, comp_with_domain6, domain6range;
 DROP DOMAIN domain1;
 DROP DOMAIN domain2;
 DROP DOMAIN domain3;
-- 
2.34.1

