From 7d9619e24048d1238c2cf49f236e746bc8810144 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 2 Jun 2025 16:50:34 +0800
Subject: [PATCH v6 3/3] fast default for domain with constraints

This is primarily done by evaluating CoerceToDomain with soft error support.

If CoerceToDomain is evaluated as false in ATExecAddColumn, the defval node's
value cannot be cast to the domain type. However, in some cases like when the
table is empty, we cannot explicitly error out in ATExecAddColumn (Phase 2).
For example, imagine add a new domain column to empty x, and the column domain
specification is ``CHECK(value > 10) DEFAULT 8``.  In such situations, the ALTER
TABLE ADD COLUMN should be success.

Thanks to commit aaaf9449ec6be62cb0d30ed3588dc384f56274bf[1],
ExprState.escontext (ErrorSaveContext) was added, and ExecEvalConstraintNotNull,
ExecEvalConstraintCheck were changed to use errsave instead of hard error.  Now
we can evaluate CoerceToDomain in a soft error way.

However we do table rewrite for domain with volatile check constraints.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
[1]: https://git.postgresql.org/cgit/postgresql.git/commit/?id=aaaf9449ec6be62cb0d30ed3588dc384f56274bf
---
 src/backend/commands/tablecmds.c           | 46 ++++++++++++++--------
 src/test/regress/expected/fast_default.out | 44 +++++++++++++++++++++
 src/test/regress/sql/fast_default.sql      | 37 +++++++++++++++++
 3 files changed, 111 insertions(+), 16 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index acf11e83c04..081930deed5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7429,15 +7429,6 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 * NULL if so, so without any modification of the tuple data we will get
 	 * the effect of NULL values in the new column.
 	 *
-	 * An exception occurs when the new column is of a domain type: the domain
-	 * might have a not-null constraint, or a check constraint that indirectly
-	 * rejects nulls.  If there are any domain constraints then we construct
-	 * an explicit NULL default value that will be passed through
-	 * CoerceToDomain processing.  (This is a tad inefficient, since it causes
-	 * rewriting the table which we really wouldn't have to do; but we do it
-	 * to preserve the historical behavior that such a failure will be raised
-	 * only if the table currently contains some rows.)
-	 *
 	 * Note: we use build_column_default, and not just the cooked default
 	 * returned by AddRelationNewConstraints, so that the right thing happens
 	 * when a datatype's default applies.
@@ -7456,6 +7447,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		bool		has_domain_constraints;
 		bool		has_missing = false;
+		bool		has_volatile = false;
 
 		/*
 		 * For an identity column, we can't use build_column_default(),
@@ -7473,8 +7465,18 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		else
 			defval = (Expr *) build_column_default(rel, attribute->attnum);
 
+		has_domain_constraints = DomainHaveVolatileConstraints(attribute->atttypid, &has_volatile);
+
+		/*
+		 * Adding column with volatile domain constraint requires table rewrite
+		 */
+		if (has_volatile)
+		{
+			Assert(has_domain_constraints);
+			tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+		}
+
 		/* Build CoerceToDomain(NULL) expression if needed */
-		has_domain_constraints = DomainHasConstraints(attribute->atttypid);
 		if (!defval && has_domain_constraints)
 		{
 			Oid			baseTypeId;
@@ -7516,14 +7518,18 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			 * Attempt to skip a complete table rewrite by storing the
 			 * specified DEFAULT value outside of the heap.  This is only
 			 * allowed for plain relations and non-generated columns, and the
-			 * default expression can't be volatile (stable is OK).  Note that
-			 * contain_volatile_functions deems CoerceToDomain immutable, but
-			 * here we consider that coercion to a domain with constraints is
-			 * volatile; else it might fail even when the table is empty.
+			 * default expression can't be volatile (stable is OK), and the
+			 * domain constraint expression can't be volatile (stable is OK).
+			 *
+			 * Note that contain_volatile_functions deems CoerceToDomain
+			 * immutable.  However we have computed CoerceToDomain is volatile
+			 * or not via DomainHaveVolatileConstraints. We use soft error
+			 * evaluation of CoerceToDomain, if evaluation failed, then set
+			 * table rewrite to true.
 			 */
 			if (rel->rd_rel->relkind == RELKIND_RELATION &&
 				!colDef->generated &&
-				!has_domain_constraints &&
+				!has_volatile &&
 				!contain_volatile_functions((Node *) defval))
 			{
 				EState	   *estate;
@@ -7533,10 +7539,18 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 				/* Evaluate the default expression */
 				estate = CreateExecutorState();
-				exprState = ExecPrepareExpr(defval, estate);
+				exprState = ExecPrepareExprSafe(defval, estate);
+
 				missingval = ExecEvalExpr(exprState,
 										  GetPerTupleExprContext(estate),
 										  &missingIsNull);
+
+				if (SOFT_ERROR_OCCURRED(exprState->escontext))
+				{
+					missingIsNull = true;
+					tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+				}
+
 				/* If it turns out NULL, nothing to do; else store it */
 				if (!missingIsNull)
 				{
diff --git a/src/test/regress/expected/fast_default.out b/src/test/regress/expected/fast_default.out
index ccbcdf8403f..aa522cf8bfa 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -317,11 +317,55 @@ SELECT a, b, length(c) = 3 as c_ok, d, e >= 10 as e_ok FROM t2;
  2 | 3 | t    | {This,is,abcd,the,real,world} | t
 (2 rows)
 
+---test fast default over domains with constraints
+CREATE DOMAIN domain5 AS int check(value > 10) default 8;
+CREATE DOMAIN domain6 as int not null;
+CREATE DOMAIN domain7 as int check(value > 10) DEFAULT random(min=>11, max=>100);
+CREATE DOMAIN domain8 as int check((value + random(min=>11::int, max=>11)) > 12);
+--tests with non-empty table.
+CREATE TABLE t3(a int);
+INSERT INTO t3 VALUES(1),(2);
+ALTER TABLE t3 ADD COLUMN b domain5; --table rewrite, then fail
+NOTICE:  rewriting table t3 for reason 2
+ERROR:  value for domain domain5 violates check constraint "domain5_check"
+ALTER TABLE t3 ADD COLUMN b domain6; --table rewrite, then fail
+NOTICE:  rewriting table t3 for reason 2
+ERROR:  domain domain6 does not allow null values
+ALTER TABLE t3 ADD COLUMN b domain5 default 12; --no table rewrite
+ALTER TABLE t3 ADD COLUMN c domain6 default 13; --no table rewrite
+--explicit column default expression override domain's default
+--expression, so no need table rewrite.
+ALTER TABLE t3 ADD COLUMN d domain7 default 14;
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM  pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+AND   atthasmissing
+ORDER BY attnum;
+ attnum | attname | atthasmissing | atthasdef | attmissingval 
+--------+---------+---------------+-----------+---------------
+      2 | b       | t             | t         | {12}
+      3 | c       | t             | t         | {13}
+      4 | d       | t             | t         | {14}
+(3 rows)
+
+--need table rewrite when we are applying domain volatile default expresion
+ALTER TABLE t3 ADD COLUMN e domain7;
+NOTICE:  rewriting table t3 for reason 2
+-- need table rewrite when new column is domain with volatile constraints.
+ALTER TABLE t3 ADD COLUMN f domain8 default 14;
+NOTICE:  rewriting table t3 for reason 2
+ALTER TABLE t3 ADD COLUMN f1 domain8;
+NOTICE:  rewriting table t3 for reason 2
 DROP TABLE t2;
+DROP TABLE t3;
 DROP DOMAIN domain1;
 DROP DOMAIN domain2;
 DROP DOMAIN domain3;
 DROP DOMAIN domain4;
+DROP DOMAIN domain5;
+DROP DOMAIN domain6;
+DROP DOMAIN domain7;
+DROP DOMAIN domain8;
 DROP FUNCTION foo(INT);
 -- Fall back to full rewrite for volatile expressions
 CREATE TABLE T(pk INT NOT NULL PRIMARY KEY);
diff --git a/src/test/regress/sql/fast_default.sql b/src/test/regress/sql/fast_default.sql
index 068dd0bc8aa..269a39edf77 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -287,11 +287,48 @@ ORDER BY attnum;
 
 SELECT a, b, length(c) = 3 as c_ok, d, e >= 10 as e_ok FROM t2;
 
+---test fast default over domains with constraints
+CREATE DOMAIN domain5 AS int check(value > 10) default 8;
+CREATE DOMAIN domain6 as int not null;
+CREATE DOMAIN domain7 as int check(value > 10) DEFAULT random(min=>11, max=>100);
+CREATE DOMAIN domain8 as int check((value + random(min=>11::int, max=>11)) > 12);
+
+--tests with non-empty table.
+CREATE TABLE t3(a int);
+INSERT INTO t3 VALUES(1),(2);
+
+ALTER TABLE t3 ADD COLUMN b domain5; --table rewrite, then fail
+ALTER TABLE t3 ADD COLUMN b domain6; --table rewrite, then fail
+ALTER TABLE t3 ADD COLUMN b domain5 default 12; --no table rewrite
+ALTER TABLE t3 ADD COLUMN c domain6 default 13; --no table rewrite
+
+--explicit column default expression override domain's default
+--expression, so no need table rewrite.
+ALTER TABLE t3 ADD COLUMN d domain7 default 14;
+
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM  pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+AND   atthasmissing
+ORDER BY attnum;
+
+--need table rewrite when we are applying domain volatile default expresion
+ALTER TABLE t3 ADD COLUMN e domain7;
+
+-- need table rewrite when new column is domain with volatile constraints.
+ALTER TABLE t3 ADD COLUMN f domain8 default 14;
+ALTER TABLE t3 ADD COLUMN f1 domain8;
+
 DROP TABLE t2;
+DROP TABLE t3;
 DROP DOMAIN domain1;
 DROP DOMAIN domain2;
 DROP DOMAIN domain3;
 DROP DOMAIN domain4;
+DROP DOMAIN domain5;
+DROP DOMAIN domain6;
+DROP DOMAIN domain7;
+DROP DOMAIN domain8;
 DROP FUNCTION foo(INT);
 
 -- Fall back to full rewrite for volatile expressions
-- 
2.34.1

