support fast default for domain with constraints

Started by jian he10 months ago9 messages
#1jian he
jian.universality@gmail.com
2 attachment(s)

hi.

Thanks to commit aaaf9449ec6be62cb0d30ed3588dc384f56274bf[1]https://git.postgresql.org/cgit/postgresql.git/commit/?id=aaaf9449ec6be62cb0d30ed3588dc384f56274bf,
ExprState.escontext (ErrorSaveContext) was added, and ExecEvalConstraintNotNull,
ExecEvalConstraintCheck were changed to use errsave instead of hard error.
Now we can use it to evaluate CoerceToDomain in a soft error way, that
is what this patch intended to do.
previously ExprState.escontext was mainly used in SQL/JSON related patches.

To achieve that, we have to populate ExprState.escontext before
passing it to ExecInitExprRec.
So I created two functions: ExecInitExprSafe, ExecPrepareExprSafe.
ExecPrepareExprSafe is an error safe variant of ExecPrepareExpr.
within ExecPrepareExprSafe, we use ExecInitExprSafe.
ExecInitExprSafe differs from ExecInitExpr is that the output
ExprState has its escontext set to a valid ErrorSaveContext.

demo:
CREATE DOMAIN domain5 AS int check(value > 10); -- stable
create domain domain6 as int not null;

CREATE TABLE t3(a int);
ALTER TABLE t3 ADD COLUMN b domain5 default 1; --should not fail.
INSERT INTO t3 DEFAULT VALUES; --should fail.
ALTER TABLE t3 DROP COLUMN b; --need drop it for the following tests
INSERT INTO t3 VALUES(1),(2);

ALTER TABLE t3 ADD COLUMN b domain6; --table rewrite. then fail.
ALTER TABLE t3 ADD COLUMN c domain6 default 13; --no table rewrite.
fast default applied. attmissingval is stored.

[1]: https://git.postgresql.org/cgit/postgresql.git/commit/?id=aaaf9449ec6be62cb0d30ed3588dc384f56274bf

Attachments:

v1-0002-support-fast-default-for-domain-with-constraints.patchtext/x-patch; charset=US-ASCII; name=v1-0002-support-fast-default-for-domain-with-constraints.patchDownload
From 37c788dd5399ffdb90373a79f787e062eeda5d80 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Wed, 5 Mar 2025 10:33:50 +0800
Subject: [PATCH v1 2/2] support fast default for domain with constraints

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

If we evaulte CoerceToDomain to false, it means in ATExecAddColumn,
the defval node evaultion value cannot be cast to the domain.
However, we cannot fail at the Phase 2 stage in cases where the table is empty.

Therefore, if an error occurred while evaultion, do not raise the error,
we signal Phase 3 to do table rewrite, error will be raise on Phase 3.

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 evaulate CoerceToDomain in a soft error way.

[1]: https://git.postgresql.org/cgit/postgresql.git/commit/?id=aaaf9449ec6be62cb0d30ed3588dc384f56274bf
---
 src/backend/commands/tablecmds.c           | 40 ++++++++----
 src/test/regress/expected/fast_default.out | 74 ++++++++++++++++++++++
 src/test/regress/sql/fast_default.sql      | 45 +++++++++++++
 3 files changed, 145 insertions(+), 14 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 13156391241..8f7b832249b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7331,15 +7331,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.
@@ -7419,13 +7410,16 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			 * 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.
+			 * contain_volatile_functions deems CoerceToDomain immutable.
+			 * We *do* support soft error of CoerceToDomain if
+			 * ExprState->escontext is not NULL.  we evaulate CoerceToDomain
+			 * (coercion to a domain with optionally constraints), if the error
+			 * happened during evaulation then we need table rewrite. we need
+			 * table rewrite not hard errror because of cases when the table is
+			 * empty.
 			 */
 			if (rel->rd_rel->relkind == RELKIND_RELATION &&
 				!colDef->generated &&
-				!has_domain_constraints &&
 				!contain_volatile_functions((Node *) defval))
 			{
 				EState	   *estate;
@@ -7435,10 +7429,28 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 				/* Evaluate the default expression */
 				estate = CreateExecutorState();
-				exprState = ExecPrepareExpr(defval, estate);
+				if (!has_domain_constraints)
+					exprState = ExecPrepareExpr(defval, estate);
+				else
+					exprState = ExecPrepareExprSafe(defval, estate);
+
 				missingval = ExecEvalExpr(exprState,
 										  GetPerTupleExprContext(estate),
 										  &missingIsNull);
+
+				/*
+				 * When has_domain_constraints is true, exprState->escontext
+				 * shoult not NULL.  If evaluating defval (CoerceToDomain Node
+				 * when attribute->atttypid is domain ) fails, it means that the
+				 * default expression cannot be coerced to that domain. This is
+				 * acceptable if the table is empty.  Otherwise, set table
+				 * rewrite and let's error out in Phase 3.
+				*/
+				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..30829a91200 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -317,11 +317,85 @@ 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 check constraint
+CREATE DOMAIN domain5 AS int check(value > 10);  -- stable
+create domain domain6 as int not null;
+create domain domain7 as int check(value is not null);
+create domain domain8 as int check(value > 10) DEFAULT random(min=>10, max=>100);
+CREATE TABLE t3(a int);
+ALTER TABLE t3 ADD COLUMN b domain5 default 1; --should not fail.
+NOTICE:  rewriting table t3 for reason 2
+INSERT INTO t3 DEFAULT VALUES; --should fail.
+ERROR:  value for domain domain5 violates check constraint "domain5_check"
+ALTER TABLE t3 DROP COLUMN b; --need drop it for the following tests
+INSERT INTO t3 VALUES(1),(2);
+ALTER TABLE t3 ADD COLUMN b domain5 default 2; --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 domain6 default 11 + NULL; --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 domain7; --table rewrite. then fail.
+NOTICE:  rewriting table t3 for reason 2
+ERROR:  value for domain domain7 violates check constraint "domain7_check"
+ALTER TABLE t3 ADD COLUMN b domain7 default 11 + NULL; --table rewrite. then fail.
+NOTICE:  rewriting table t3 for reason 2
+ERROR:  value for domain domain7 violates check constraint "domain7_check"
+ALTER TABLE t3 ADD COLUMN b domain5 default 12; --no table rewrite.
+ALTER TABLE t3 ADD COLUMN c domain6 default 13; --no table rewrite.
+ALTER TABLE t3 ADD COLUMN d domain7 default 14; --no table rewrite.
+--no table rewrite. we consider explicit column default expression, not domain default
+ALTER TABLE t3 ADD COLUMN e domain8 default 15;
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+ORDER BY attnum;
+ attnum | attname | atthasmissing | atthasdef | attmissingval 
+--------+---------+---------------+-----------+---------------
+      1 | a       | f             | f         | 
+      3 | b       | t             | t         | {12}
+      4 | c       | t             | t         | {13}
+      5 | d       | t             | t         | {14}
+      6 | e       | t             | t         | {15}
+(5 rows)
+
+--table rewrite. we are applying domain volatile default expresion
+ALTER TABLE t3 ADD COLUMN f domain8;
+NOTICE:  rewriting table t3 for reason 2
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+ORDER BY attnum;
+ attnum | attname | atthasmissing | atthasdef | attmissingval 
+--------+---------+---------------+-----------+---------------
+      1 | a       | f             | f         | 
+      3 | b       | f             | t         | 
+      4 | c       | f             | t         | 
+      5 | d       | f             | t         | 
+      6 | e       | f             | t         | 
+      7 | f       | f             | f         | 
+(6 rows)
+
+SELECT a,b,c,d,e, f > 10 as f_ok FROM t3 ORDER BY a;
+ a | b  | c  | d  | e  | f_ok 
+---+----+----+----+----+------
+ 1 | 12 | 13 | 14 | 15 | t
+ 2 | 12 | 13 | 14 | 15 | t
+(2 rows)
+
 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..5aaa7e7818c 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -287,11 +287,56 @@ 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 check constraint
+CREATE DOMAIN domain5 AS int check(value > 10);  -- stable
+create domain domain6 as int not null;
+create domain domain7 as int check(value is not null);
+create domain domain8 as int check(value > 10) DEFAULT random(min=>10, max=>100);
+
+CREATE TABLE t3(a int);
+ALTER TABLE t3 ADD COLUMN b domain5 default 1; --should not fail.
+INSERT INTO t3 DEFAULT VALUES; --should fail.
+
+ALTER TABLE t3 DROP COLUMN b; --need drop it for the following tests
+INSERT INTO t3 VALUES(1),(2);
+
+ALTER TABLE t3 ADD COLUMN b domain5 default 2; --table rewrite. then fail.
+ALTER TABLE t3 ADD COLUMN b domain6; --table rewrite. then fail.
+ALTER TABLE t3 ADD COLUMN b domain6 default 11 + NULL; --table rewrite. then fail.
+ALTER TABLE t3 ADD COLUMN b domain7; --table rewrite. then fail.
+ALTER TABLE t3 ADD COLUMN b domain7 default 11 + NULL; --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.
+ALTER TABLE t3 ADD COLUMN d domain7 default 14; --no table rewrite.
+--no table rewrite. we consider explicit column default expression, not domain default
+ALTER TABLE t3 ADD COLUMN e domain8 default 15;
+
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+ORDER BY attnum;
+
+--table rewrite. we are applying domain volatile default expresion
+ALTER TABLE t3 ADD COLUMN f domain8;
+
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+ORDER BY attnum;
+
+SELECT a,b,c,d,e, f > 10 as f_ok FROM t3 ORDER BY a;
+
 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

v1-0001-add-soft-error-variant-of-ExecPrepareExpr-ExecIni.patchtext/x-patch; charset=US-ASCII; name=v1-0001-add-soft-error-variant-of-ExecPrepareExpr-ExecIni.patchDownload
From 44e49d3c070c3744f75540a7d48af723a49926f0 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Wed, 5 Mar 2025 10:26:10 +0800
Subject: [PATCH v1 1/2] add soft error variant of ExecPrepareExpr,
 ExecInitExpr

ExecPrepareExprSafe and ExecInitExprSafe.
ExecPrepareExprSafe initialize for expression execution with soft error support.
not all expression node support it. some like CoerceToDomain do support it.

XXX more comments.
---
 src/backend/executor/execExpr.c | 63 +++++++++++++++++++++++++++++++++
 src/include/executor/executor.h |  2 ++
 2 files changed, 65 insertions(+)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 03566c4d181..04f8b839d30 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -170,6 +170,46 @@ ExecInitExpr(Expr *node, PlanState *parent)
 	return state;
 }
 
+/*
+ * ExecInitExpr: soft error variant of ExecInitExpr.
+ * use it only for expression nodes support soft errors, not all expression
+ * nodes support it.
+*/
+ExprState *
+ExecInitExprSafe(Expr *node, PlanState *parent)
+{
+	ExprState  *state;
+	ExprEvalStep scratch = {0};
+
+	/* Special case: NULL expression produces a NULL ExprState pointer */
+	if (node == NULL)
+		return NULL;
+
+	/* Initialize ExprState with empty step list */
+	state = makeNode(ExprState);
+	state->expr = node;
+	state->parent = parent;
+	state->ext_params = NULL;
+	state->escontext = makeNode(ErrorSaveContext);
+	state->escontext->type = T_ErrorSaveContext;
+	state->escontext->error_occurred = false;
+	state->escontext->details_wanted = true;
+
+	/* Insert setup steps as needed */
+	ExecCreateExprSetupSteps(state, (Node *) node);
+
+	/* Compile the expression proper */
+	ExecInitExprRec(node, state, &state->resvalue, &state->resnull);
+
+	/* Finally, append a DONE step */
+	scratch.opcode = EEOP_DONE;
+	ExprEvalPushStep(state, &scratch);
+
+	ExecReadyExpr(state);
+
+	return state;
+}
+
 /*
  * ExecInitExprWithParams: prepare a standalone expression tree for execution
  *
@@ -778,6 +818,29 @@ ExecPrepareExpr(Expr *node, EState *estate)
 	return result;
 }
 
+/*
+ * ExecPrepareExprSafe: soft error variant of ExecPrepareExpr.
+ *
+ * use it when expression node *support* soft error expression execution.
+ * ExecPrepareExpr comments apply to here too.
+ */
+ExprState *
+ExecPrepareExprSafe(Expr *node, EState *estate)
+{
+	ExprState  *result;
+	MemoryContext oldcontext;
+
+	oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
+
+	node = expression_planner(node);
+
+	result = ExecInitExprSafe(node, NULL);
+
+	MemoryContextSwitchTo(oldcontext);
+
+	return result;
+}
+
 /*
  * ExecPrepareQual --- initialize for qual execution outside a normal
  * Plan tree context.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index d12e3f451d2..b7ab95437fe 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -305,6 +305,7 @@ ExecProcNode(PlanState *node)
  * prototypes from functions in execExpr.c
  */
 extern ExprState *ExecInitExpr(Expr *node, PlanState *parent);
+extern ExprState *ExecInitExprSafe(Expr *node, PlanState *parent);
 extern ExprState *ExecInitExprWithParams(Expr *node, ParamListInfo ext_params);
 extern ExprState *ExecInitQual(List *qual, PlanState *parent);
 extern ExprState *ExecInitCheck(List *qual, PlanState *parent);
@@ -353,6 +354,7 @@ extern ProjectionInfo *ExecBuildUpdateProjection(List *targetList,
 												 TupleTableSlot *slot,
 												 PlanState *parent);
 extern ExprState *ExecPrepareExpr(Expr *node, EState *estate);
+extern ExprState *ExecPrepareExprSafe(Expr *node, EState *estate);
 extern ExprState *ExecPrepareQual(List *qual, EState *estate);
 extern ExprState *ExecPrepareCheck(List *qual, EState *estate);
 extern List *ExecPrepareExprList(List *nodes, EState *estate);
-- 
2.34.1

#2Tom Lane
tgl@sss.pgh.pa.us
In reply to: jian he (#1)
Re: support fast default for domain with constraints

jian he <jian.universality@gmail.com> writes:

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 use it to evaluate CoerceToDomain in a soft error way, that
is what this patch intended to do.

This patch appears to summarily throw away a couple of
backwards-compatibility concerns that the previous round
took care to preserve:

* not throwing an error if the default would fail the domain
constraints, but the table is empty so there is no need to
instantiate the default.

* not assuming that the domain constraints are immutable.

Now it's fair to question how important the second point is
considering that we mostly treat domain constraints as immutable
elsewhere.  But I think the first point has actual practical uses
--- for example, if you want to set things up so that inserts must
specify that column explicitly.  So I don't think it's okay to
discard that behavior.

Maybe we need a regression test case demonstrating that that
behavior exists, to discourage people from breaking it ...

regards, tom lane

#3jian he
jian.universality@gmail.com
In reply to: Tom Lane (#2)
3 attachment(s)
Re: support fast default for domain with constraints

On Wed, Mar 5, 2025 at 11:13 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

This patch appears to summarily throw away a couple of
backwards-compatibility concerns that the previous round
took care to preserve:

* not throwing an error if the default would fail the domain
constraints, but the table is empty so there is no need to
instantiate the default.

hi. Thanks for pointing this out.
I noticed an empty table scarenio, but didn't check it thoroughly.
The attached patch preserves this backwards-compatibility.
now it's aligned with master behavior, i think.

main gotcha is:
ALTER TABLE ADD COLUMN...
If no explicitly DEFAULT, the defval either comes from pg_type.typdefaultbin,
or constructed via makeNullConst branch.
In that case, we need to use soft error evaluation, because we allow
these cases for an empty table;
In other cases, we can directly evaluate explicitly the DEFAULT clause.

* not assuming that the domain constraints are immutable.

Now it's fair to question how important the second point is
considering that we mostly treat domain constraints as immutable
elsewhere.  But I think the first point has actual practical uses
--- for example, if you want to set things up so that inserts must
specify that column explicitly.  So I don't think it's okay to
discard that behavior.

in v2-0003. I created a new function:
bool DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile)
within DomainHaveVolatileConstraints
i use contain_volatile_functions to test whether check_expr is volatile or not.
contain_volatile_functions won't be expensive, i think.

if true then have_volatile is set to true.
if have_volatile is true then we need table rewrite.

Attachments:

v2-0002-fast-default-for-domain-with-constraints.patchtext/x-patch; charset=US-ASCII; name=v2-0002-fast-default-for-domain-with-constraints.patchDownload
From 40364a9f3926a9ebc8cad4534ab2221e2a1d2574 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Wed, 5 Mar 2025 20:38:27 +0800
Subject: [PATCH v2 2/3] fast default for domain with constraints

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

If we evaluate CoerceToDomain to false, it means in ATExecAddColumn,
the defval node evaluation value cannot be cast to the domain.
However, we cannot fail at the Phase 2 stage in cases where the table is empty.

Therefore, if an error occurred while evaluation, do not raise the error,
we signal Phase 3 to do table rewrite, error will be raised on Phase 3.

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.

NOTE: this patch does not consider 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           | 43 ++++++++--
 src/test/regress/expected/fast_default.out | 91 ++++++++++++++++++++++
 src/test/regress/sql/fast_default.sql      | 57 ++++++++++++++
 3 files changed, 186 insertions(+), 5 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8826ca5c32c..f7c8348c7ff 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7331,6 +7331,7 @@ 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.
 	 *
+	 * XXXX this para should be removed?
 	 * 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
@@ -7358,6 +7359,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		bool		has_domain_constraints;
 		bool		has_missing = false;
+		bool		no_explicit_defval = false;
 
 		/*
 		 * For an identity column, we can't use build_column_default(),
@@ -7375,6 +7377,21 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		else
 			defval = (Expr *) build_column_default(rel, attribute->attnum);
 
+		/*
+		 * if defval is there, atthasdef is false, that means the defval comes
+		 * from domain default value, no explicit DEFAULT expression specified.
+		 * In that case we need evaluate defval error safe way, so
+		 * domain specification such as ``check(value > 10) default 8 ``
+		 * can be added to empty table.
+		*/
+		if (defval)
+		{
+			TupleDesc	rd_att = rel->rd_att;
+			Form_pg_attribute att_tup = TupleDescAttr(rd_att, attribute->attnum - 1);
+			if (!att_tup->atthasdef)
+				no_explicit_defval = true;
+		}
+
 		/* Build CoerceToDomain(NULL) expression if needed */
 		has_domain_constraints = DomainHasConstraints(attribute->atttypid);
 		if (!defval && has_domain_constraints)
@@ -7397,6 +7414,12 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 													-1);
 			if (defval == NULL) /* should not happen */
 				elog(ERROR, "failed to coerce base type to domain");
+			/*
+			 * if domain have not-null constraint or check constraint
+			 * equivalent to not-null, we only want it failure when table have
+			 * some rows. so do it in soft error way.
+			*/
+			no_explicit_defval = true;
 		}
 
 		if (defval)
@@ -7419,13 +7442,13 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			 * 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.
+			 * contain_volatile_functions deems CoerceToDomain immutable.
+			 * We do support soft error evaluation of CoerceToDomain if
+			 * ExprState->escontext is not NULL. In that case if evaluation failed,
+			 * set table rewrite to true, let's fail on Phase 3.
 			 */
 			if (rel->rd_rel->relkind == RELKIND_RELATION &&
 				!colDef->generated &&
-				!has_domain_constraints &&
 				!contain_volatile_functions((Node *) defval))
 			{
 				EState	   *estate;
@@ -7435,10 +7458,20 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 				/* Evaluate the default expression */
 				estate = CreateExecutorState();
-				exprState = ExecPrepareExpr(defval, estate);
+				if (!no_explicit_defval)
+					exprState = ExecPrepareExpr(defval, estate);
+				else
+					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..1ab610a1410 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -317,11 +317,102 @@ 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 check constraint
+CREATE DOMAIN domain5 AS int check(value > 10) default 8;
+create domain domain6 as int not null;
+create domain domain7 as int check(value is not null);
+create domain domain8 as int check(value > 10) DEFAULT random(min=>10, max=>100);
+create domain domain9 as int check(value is not null) default 9;
+CREATE TABLE t3(a int);
+ALTER TABLE t3 ADD COLUMN b domain5 default 1; --error
+ERROR:  value for domain domain5 violates check constraint "domain5_check"
+ALTER TABLE t3 ADD COLUMN b domain6 default NULL; --error
+ERROR:  domain domain6 does not allow null values
+ALTER TABLE t3 ADD COLUMN b domain6 default 11 + NULL; --error
+ERROR:  domain domain6 does not allow null values
+ALTER TABLE t3 ADD COLUMN b domain7 default 11 + NULL; --error
+ERROR:  value for domain domain7 violates check constraint "domain7_check"
+ALTER TABLE t3 ADD COLUMN b domain6; --ok. because t3 is empty
+NOTICE:  rewriting table t3 for reason 2
+ALTER TABLE t3 ADD COLUMN c domain7; --ok. because t3 is empty
+NOTICE:  rewriting table t3 for reason 2
+ALTER TABLE t3 ADD COLUMN d domain5; --ok. because t3 is empty
+NOTICE:  rewriting table t3 for reason 2
+INSERT INTO t3(a,b,c,d) values(1,2, 3, default); --should fail
+ERROR:  value for domain domain5 violates check constraint "domain5_check"
+INSERT INTO t3(a,b,c,d) values(1,default, 3, 12); --should fail
+ERROR:  domain domain6 does not allow null values
+INSERT INTO t3(a,b,c,d) values(1,2, default, 12); --should fail
+ERROR:  value for domain domain7 violates check constraint "domain7_check"
+INSERT INTO t3(a,b,c,d) values(1,2, 3, 12); --ok
+DROP TABLE t3;
+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 domain7; --table rewrite, then fail
+NOTICE:  rewriting table t3 for reason 2
+ERROR:  value for domain domain7 violates check constraint "domain7_check"
+ALTER TABLE t3 ADD COLUMN b domain5 default 12; --no table rewrite
+ALTER TABLE t3 ADD COLUMN c domain6 default 13; --no table rewrite
+ALTER TABLE t3 ADD COLUMN d domain7 default 14; --no table rewrite
+--no table rewrite we consider explicit column default expression, not domain default
+ALTER TABLE t3 ADD COLUMN e domain8 default 15;
+ALTER TABLE t3 ADD COLUMN f domain9; --no table rewrite
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+ORDER BY attnum;
+ attnum | attname | atthasmissing | atthasdef | attmissingval 
+--------+---------+---------------+-----------+---------------
+      1 | a       | f             | f         | 
+      2 | b       | t             | t         | {12}
+      3 | c       | t             | t         | {13}
+      4 | d       | t             | t         | {14}
+      5 | e       | t             | t         | {15}
+      6 | f       | t             | f         | {9}
+(6 rows)
+
+--table rewrite. we are applying domain volatile default expresion
+ALTER TABLE t3 ADD COLUMN g domain8;
+NOTICE:  rewriting table t3 for reason 2
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+ORDER BY attnum;
+ attnum | attname | atthasmissing | atthasdef | attmissingval 
+--------+---------+---------------+-----------+---------------
+      1 | a       | f             | f         | 
+      2 | b       | f             | t         | 
+      3 | c       | f             | t         | 
+      4 | d       | f             | t         | 
+      5 | e       | f             | t         | 
+      6 | f       | f             | f         | 
+      7 | g       | f             | f         | 
+(7 rows)
+
+SELECT a,b,c,d,e,f, g > 10 as f_ok FROM t3 ORDER BY a;
+ a | b  | c  | d  | e  | f | f_ok 
+---+----+----+----+----+---+------
+ 1 | 12 | 13 | 14 | 15 | 9 | t
+ 2 | 12 | 13 | 14 | 15 | 9 | t
+(2 rows)
+
 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 DOMAIN domain9;
 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..e3139ce8b15 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -287,11 +287,68 @@ 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 check constraint
+CREATE DOMAIN domain5 AS int check(value > 10) default 8;
+create domain domain6 as int not null;
+create domain domain7 as int check(value is not null);
+create domain domain8 as int check(value > 10) DEFAULT random(min=>10, max=>100);
+create domain domain9 as int check(value is not null) default 9;
+
+CREATE TABLE t3(a int);
+ALTER TABLE t3 ADD COLUMN b domain5 default 1; --error
+ALTER TABLE t3 ADD COLUMN b domain6 default NULL; --error
+ALTER TABLE t3 ADD COLUMN b domain6 default 11 + NULL; --error
+ALTER TABLE t3 ADD COLUMN b domain7 default 11 + NULL; --error
+
+ALTER TABLE t3 ADD COLUMN b domain6; --ok. because t3 is empty
+ALTER TABLE t3 ADD COLUMN c domain7; --ok. because t3 is empty
+ALTER TABLE t3 ADD COLUMN d domain5; --ok. because t3 is empty
+INSERT INTO t3(a,b,c,d) values(1,2, 3, default); --should fail
+INSERT INTO t3(a,b,c,d) values(1,default, 3, 12); --should fail
+INSERT INTO t3(a,b,c,d) values(1,2, default, 12); --should fail
+INSERT INTO t3(a,b,c,d) values(1,2, 3, 12); --ok
+
+DROP TABLE t3;
+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 domain7; --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
+ALTER TABLE t3 ADD COLUMN d domain7 default 14; --no table rewrite
+--no table rewrite we consider explicit column default expression, not domain default
+ALTER TABLE t3 ADD COLUMN e domain8 default 15;
+ALTER TABLE t3 ADD COLUMN f domain9; --no table rewrite
+
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+ORDER BY attnum;
+
+--table rewrite. we are applying domain volatile default expresion
+ALTER TABLE t3 ADD COLUMN g domain8;
+
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+ORDER BY attnum;
+
+SELECT a,b,c,d,e,f, g > 10 as f_ok FROM t3 ORDER BY a;
+
 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 DOMAIN domain9;
 DROP FUNCTION foo(INT);
 
 -- Fall back to full rewrite for volatile expressions
-- 
2.34.1

v2-0001-soft-error-variant-of-ExecPrepareExpr-ExecInitExp.patchtext/x-patch; charset=US-ASCII; name=v2-0001-soft-error-variant-of-ExecPrepareExpr-ExecInitExp.patchDownload
From ae57f10e34862295b00fdd1a0560995a1597c692 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Wed, 5 Mar 2025 20:37:31 +0800
Subject: [PATCH v2 1/3] soft error variant of ExecPrepareExpr, ExecInitExpr

ExecPrepareExprSafe and ExecInitExprSafe.
ExecPrepareExprSafe initialize for expression execution with soft error support.
not all expression node support it. some like CoerceToDomain do support it.

XXX more comments.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/executor/execExpr.c | 63 +++++++++++++++++++++++++++++++++
 src/include/executor/executor.h |  2 ++
 2 files changed, 65 insertions(+)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 03566c4d181..04f8b839d30 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -170,6 +170,46 @@ ExecInitExpr(Expr *node, PlanState *parent)
 	return state;
 }
 
+/*
+ * ExecInitExpr: soft error variant of ExecInitExpr.
+ * use it only for expression nodes support soft errors, not all expression
+ * nodes support it.
+*/
+ExprState *
+ExecInitExprSafe(Expr *node, PlanState *parent)
+{
+	ExprState  *state;
+	ExprEvalStep scratch = {0};
+
+	/* Special case: NULL expression produces a NULL ExprState pointer */
+	if (node == NULL)
+		return NULL;
+
+	/* Initialize ExprState with empty step list */
+	state = makeNode(ExprState);
+	state->expr = node;
+	state->parent = parent;
+	state->ext_params = NULL;
+	state->escontext = makeNode(ErrorSaveContext);
+	state->escontext->type = T_ErrorSaveContext;
+	state->escontext->error_occurred = false;
+	state->escontext->details_wanted = true;
+
+	/* Insert setup steps as needed */
+	ExecCreateExprSetupSteps(state, (Node *) node);
+
+	/* Compile the expression proper */
+	ExecInitExprRec(node, state, &state->resvalue, &state->resnull);
+
+	/* Finally, append a DONE step */
+	scratch.opcode = EEOP_DONE;
+	ExprEvalPushStep(state, &scratch);
+
+	ExecReadyExpr(state);
+
+	return state;
+}
+
 /*
  * ExecInitExprWithParams: prepare a standalone expression tree for execution
  *
@@ -778,6 +818,29 @@ ExecPrepareExpr(Expr *node, EState *estate)
 	return result;
 }
 
+/*
+ * ExecPrepareExprSafe: soft error variant of ExecPrepareExpr.
+ *
+ * use it when expression node *support* soft error expression execution.
+ * ExecPrepareExpr comments apply to here too.
+ */
+ExprState *
+ExecPrepareExprSafe(Expr *node, EState *estate)
+{
+	ExprState  *result;
+	MemoryContext oldcontext;
+
+	oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
+
+	node = expression_planner(node);
+
+	result = ExecInitExprSafe(node, NULL);
+
+	MemoryContextSwitchTo(oldcontext);
+
+	return result;
+}
+
 /*
  * ExecPrepareQual --- initialize for qual execution outside a normal
  * Plan tree context.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index d12e3f451d2..b7ab95437fe 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -305,6 +305,7 @@ ExecProcNode(PlanState *node)
  * prototypes from functions in execExpr.c
  */
 extern ExprState *ExecInitExpr(Expr *node, PlanState *parent);
+extern ExprState *ExecInitExprSafe(Expr *node, PlanState *parent);
 extern ExprState *ExecInitExprWithParams(Expr *node, ParamListInfo ext_params);
 extern ExprState *ExecInitQual(List *qual, PlanState *parent);
 extern ExprState *ExecInitCheck(List *qual, PlanState *parent);
@@ -353,6 +354,7 @@ extern ProjectionInfo *ExecBuildUpdateProjection(List *targetList,
 												 TupleTableSlot *slot,
 												 PlanState *parent);
 extern ExprState *ExecPrepareExpr(Expr *node, EState *estate);
+extern ExprState *ExecPrepareExprSafe(Expr *node, EState *estate);
 extern ExprState *ExecPrepareQual(List *qual, EState *estate);
 extern ExprState *ExecPrepareCheck(List *qual, EState *estate);
 extern List *ExecPrepareExprList(List *nodes, EState *estate);
-- 
2.34.1

v2-0003-no-fast-default-for-domain-with-voltile-constrain.patchtext/x-patch; charset=US-ASCII; name=v2-0003-no-fast-default-for-domain-with-voltile-constrain.patchDownload
From f1791a42aebdfd5be76682c200e71aeafe75e7c0 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Wed, 5 Mar 2025 20:39:42 +0800
Subject: [PATCH v2 3/3] no fast default for domain with voltile constraints

this patch force table rewrite when ALTER TABLE ADD COLUMN,
the to be added column is a domain with volatile check constraints.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/commands/tablecmds.c           | 11 +++++-
 src/backend/utils/cache/typcache.c         | 37 +++++++++++++++++++
 src/include/utils/typcache.h               |  1 +
 src/test/regress/expected/fast_default.out | 42 ++++++++++++++++++++++
 src/test/regress/sql/fast_default.sql      | 31 ++++++++++++++++
 5 files changed, 121 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f7c8348c7ff..8edd7da5d74 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7360,6 +7360,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		bool		has_domain_constraints;
 		bool		has_missing = false;
 		bool		no_explicit_defval = false;
+		bool		has_volatile = false;
 
 		/*
 		 * For an identity column, we can't use build_column_default(),
@@ -7393,7 +7394,14 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		}
 
 		/* Build CoerceToDomain(NULL) expression if needed */
-		has_domain_constraints = DomainHasConstraints(attribute->atttypid);
+		has_domain_constraints = DomainHaveVolatileConstraints(attribute->atttypid, &has_volatile);
+
+		if (has_volatile)
+		{
+			Assert(has_domain_constraints);
+			tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+		}
+
 		if (!defval && has_domain_constraints)
 		{
 			Oid			baseTypeId;
@@ -7449,6 +7457,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			 */
 			if (rel->rd_rel->relkind == RELKIND_RELATION &&
 				!colDef->generated &&
+				!has_volatile &&
 				!contain_volatile_functions((Node *) defval))
 			{
 				EState	   *estate;
diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index 5a3b3788d02..617d0ec27cf 100644
--- a/src/backend/utils/cache/typcache.c
+++ b/src/backend/utils/cache/typcache.c
@@ -1498,6 +1498,43 @@ DomainHasConstraints(Oid type_id)
 }
 
 
+/*
+ * Returns true if the Domain has any constraints.
+ * To check for the presence of volatile constraints, ensure
+ * have_volatile is not NULL. If a volatile constraint exists,
+ * have_volatile will be true.
+ */
+bool
+DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile)
+{
+	TypeCacheEntry *typentry;
+
+	/*
+	 * Note: a side effect is to cause the typcache's domain data to become
+	 * valid.  This is fine since we'll likely need it soon if there is any.
+	 */
+	typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO);
+
+	if (typentry->domainData != NULL)
+	{
+		ListCell   *lc;
+
+		foreach(lc, typentry->domainData->constraints)
+		{
+			DomainConstraintState *r = (DomainConstraintState *) lfirst(lc);
+
+			if (r->constrainttype == DOM_CONSTRAINT_CHECK &&
+				contain_volatile_functions((Node *) r->check_expr))
+			{
+				*have_volatile = true;
+				break;
+			}
+		}
+		return true;
+	}
+	return false;
+}
+
 /*
  * array_element_has_equality and friends are helper routines to check
  * whether we should believe that array_eq and related functions will work
diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h
index 562a581333a..36257c4240c 100644
--- a/src/include/utils/typcache.h
+++ b/src/include/utils/typcache.h
@@ -183,6 +183,7 @@ extern void InitDomainConstraintRef(Oid type_id, DomainConstraintRef *ref,
 extern void UpdateDomainConstraintRef(DomainConstraintRef *ref);
 
 extern bool DomainHasConstraints(Oid type_id);
+extern bool DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile);
 
 extern TupleDesc lookup_rowtype_tupdesc(Oid type_id, int32 typmod);
 
diff --git a/src/test/regress/expected/fast_default.out b/src/test/regress/expected/fast_default.out
index 1ab610a1410..8d16beab74c 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -402,8 +402,48 @@ SELECT a,b,c,d,e,f, g > 10 as f_ok FROM t3 ORDER BY a;
  2 | 12 | 13 | 14 | 15 | 9 | t
 (2 rows)
 
+------test table rewrite for volatile domain constraints.
+create domain domain10 as int check((value + random(min=>11::int, max=>11)) > 12); --volatile
+create domain domain11 as int check((value + random(min=>11::int, max=>11)) > 12) default 1; --volatile
+--test with empty table
+CREATE TABLE t4(a int);
+ALTER TABLE t4 ADD COLUMN b domain10 default -1;
+NOTICE:  rewriting table t4 for reason 2
+ALTER TABLE t4 ADD COLUMN c domain11 default -1;
+NOTICE:  rewriting table t4 for reason 2
+ALTER TABLE t4 ADD COLUMN d domain11;
+NOTICE:  rewriting table t4 for reason 2
+INSERT INTO t4 default values;
+ERROR:  value for domain domain10 violates check constraint "domain10_check"
+DROP TABLE T4;
+CREATE TABLE t4(a int);
+INSERT INTO t4 VALUES(1),(2);
+--all these will table rewrite then error out.
+ALTER TABLE t4 ADD COLUMN b domain10 default -1;
+NOTICE:  rewriting table t4 for reason 2
+ERROR:  value for domain domain10 violates check constraint "domain10_check"
+ALTER TABLE t4 ADD COLUMN b domain11 default -1;
+NOTICE:  rewriting table t4 for reason 2
+ERROR:  value for domain domain11 violates check constraint "domain11_check"
+ALTER TABLE t4 ADD COLUMN b domain11;
+NOTICE:  rewriting table t4 for reason 2
+ERROR:  value for domain domain11 violates check constraint "domain11_check"
+--all these will table rewrite and be ok.
+ALTER TABLE t4 ADD COLUMN b domain10; --default to NULL
+NOTICE:  rewriting table t4 for reason 2
+ALTER TABLE t4 ADD COLUMN c domain10 default 14;
+NOTICE:  rewriting table t4 for reason 2
+SELECT COUNT(*) AS expect_zero
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't4'::regclass AND attmissingval IS NOT NULL;
+ expect_zero 
+-------------
+           0
+(1 row)
+
 DROP TABLE t2;
 DROP TABLE t3;
+DROP TABLE t4;
 DROP DOMAIN domain1;
 DROP DOMAIN domain2;
 DROP DOMAIN domain3;
@@ -413,6 +453,8 @@ DROP DOMAIN domain6;
 DROP DOMAIN domain7;
 DROP DOMAIN domain8;
 DROP DOMAIN domain9;
+DROP DOMAIN domain10;
+DROP DOMAIN domain11;
 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 e3139ce8b15..e080a38daa8 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -338,8 +338,37 @@ ORDER BY attnum;
 
 SELECT a,b,c,d,e,f, g > 10 as f_ok FROM t3 ORDER BY a;
 
+------test table rewrite for volatile domain constraints.
+create domain domain10 as int check((value + random(min=>11::int, max=>11)) > 12); --volatile
+create domain domain11 as int check((value + random(min=>11::int, max=>11)) > 12) default 1; --volatile
+
+--test with empty table
+CREATE TABLE t4(a int);
+ALTER TABLE t4 ADD COLUMN b domain10 default -1;
+ALTER TABLE t4 ADD COLUMN c domain11 default -1;
+ALTER TABLE t4 ADD COLUMN d domain11;
+INSERT INTO t4 default values;
+DROP TABLE T4;
+
+
+CREATE TABLE t4(a int);
+INSERT INTO t4 VALUES(1),(2);
+--all these will table rewrite then error out.
+ALTER TABLE t4 ADD COLUMN b domain10 default -1;
+ALTER TABLE t4 ADD COLUMN b domain11 default -1;
+ALTER TABLE t4 ADD COLUMN b domain11;
+
+--all these will table rewrite and be ok.
+ALTER TABLE t4 ADD COLUMN b domain10; --default to NULL
+ALTER TABLE t4 ADD COLUMN c domain10 default 14;
+
+SELECT COUNT(*) AS expect_zero
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't4'::regclass AND attmissingval IS NOT NULL;
+
 DROP TABLE t2;
 DROP TABLE t3;
+DROP TABLE t4;
 DROP DOMAIN domain1;
 DROP DOMAIN domain2;
 DROP DOMAIN domain3;
@@ -349,6 +378,8 @@ DROP DOMAIN domain6;
 DROP DOMAIN domain7;
 DROP DOMAIN domain8;
 DROP DOMAIN domain9;
+DROP DOMAIN domain10;
+DROP DOMAIN domain11;
 DROP FUNCTION foo(INT);
 
 -- Fall back to full rewrite for volatile expressions
-- 
2.34.1

#4jian he
jian.universality@gmail.com
In reply to: jian he (#3)
3 attachment(s)
Re: support fast default for domain with constraints

hi.

rearrange the patch.
v3-0001 and v3-0002 is preparare patches.
v3-0001 add function: ExecPrepareExprSafe and ExecInitExprSafe.
v3-0002 add function: DomainHaveVolatileConstraints

v3-0003 tests and apply fast default for domain with constraints.
v3-0003 table with empty rows aligned with master behavior.
also no table rewrite if the domain has volatile check constraints,
so less surprising behavior.

Attachments:

v3-0001-soft-error-variant-of-ExecPrepareExpr-ExecInitExp.patchtext/x-patch; charset=US-ASCII; name=v3-0001-soft-error-variant-of-ExecPrepareExpr-ExecInitExp.patchDownload
From b2b42a27bcad670e3f5843b9db4ee369e3f30d9c Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Wed, 5 Mar 2025 20:37:31 +0800
Subject: [PATCH v3 1/3] soft error variant of ExecPrepareExpr, ExecInitExpr

ExecPrepareExprSafe and ExecInitExprSafe.
ExecPrepareExprSafe initialize for expression execution with soft error support.
not all expression node support it. some like CoerceToDomain do support it.

XXX more comments.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/executor/execExpr.c | 63 +++++++++++++++++++++++++++++++++
 src/include/executor/executor.h |  2 ++
 2 files changed, 65 insertions(+)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 03566c4d181..04f8b839d30 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -170,6 +170,46 @@ ExecInitExpr(Expr *node, PlanState *parent)
 	return state;
 }
 
+/*
+ * ExecInitExpr: soft error variant of ExecInitExpr.
+ * use it only for expression nodes support soft errors, not all expression
+ * nodes support it.
+*/
+ExprState *
+ExecInitExprSafe(Expr *node, PlanState *parent)
+{
+	ExprState  *state;
+	ExprEvalStep scratch = {0};
+
+	/* Special case: NULL expression produces a NULL ExprState pointer */
+	if (node == NULL)
+		return NULL;
+
+	/* Initialize ExprState with empty step list */
+	state = makeNode(ExprState);
+	state->expr = node;
+	state->parent = parent;
+	state->ext_params = NULL;
+	state->escontext = makeNode(ErrorSaveContext);
+	state->escontext->type = T_ErrorSaveContext;
+	state->escontext->error_occurred = false;
+	state->escontext->details_wanted = true;
+
+	/* Insert setup steps as needed */
+	ExecCreateExprSetupSteps(state, (Node *) node);
+
+	/* Compile the expression proper */
+	ExecInitExprRec(node, state, &state->resvalue, &state->resnull);
+
+	/* Finally, append a DONE step */
+	scratch.opcode = EEOP_DONE;
+	ExprEvalPushStep(state, &scratch);
+
+	ExecReadyExpr(state);
+
+	return state;
+}
+
 /*
  * ExecInitExprWithParams: prepare a standalone expression tree for execution
  *
@@ -778,6 +818,29 @@ ExecPrepareExpr(Expr *node, EState *estate)
 	return result;
 }
 
+/*
+ * ExecPrepareExprSafe: soft error variant of ExecPrepareExpr.
+ *
+ * use it when expression node *support* soft error expression execution.
+ * ExecPrepareExpr comments apply to here too.
+ */
+ExprState *
+ExecPrepareExprSafe(Expr *node, EState *estate)
+{
+	ExprState  *result;
+	MemoryContext oldcontext;
+
+	oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
+
+	node = expression_planner(node);
+
+	result = ExecInitExprSafe(node, NULL);
+
+	MemoryContextSwitchTo(oldcontext);
+
+	return result;
+}
+
 /*
  * ExecPrepareQual --- initialize for qual execution outside a normal
  * Plan tree context.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index d12e3f451d2..b7ab95437fe 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -305,6 +305,7 @@ ExecProcNode(PlanState *node)
  * prototypes from functions in execExpr.c
  */
 extern ExprState *ExecInitExpr(Expr *node, PlanState *parent);
+extern ExprState *ExecInitExprSafe(Expr *node, PlanState *parent);
 extern ExprState *ExecInitExprWithParams(Expr *node, ParamListInfo ext_params);
 extern ExprState *ExecInitQual(List *qual, PlanState *parent);
 extern ExprState *ExecInitCheck(List *qual, PlanState *parent);
@@ -353,6 +354,7 @@ extern ProjectionInfo *ExecBuildUpdateProjection(List *targetList,
 												 TupleTableSlot *slot,
 												 PlanState *parent);
 extern ExprState *ExecPrepareExpr(Expr *node, EState *estate);
+extern ExprState *ExecPrepareExprSafe(Expr *node, EState *estate);
 extern ExprState *ExecPrepareQual(List *qual, EState *estate);
 extern ExprState *ExecPrepareCheck(List *qual, EState *estate);
 extern List *ExecPrepareExprList(List *nodes, EState *estate);
-- 
2.34.1

v3-0002-add-function-DomainHaveVolatileConstraints.patchtext/x-patch; charset=US-ASCII; name=v3-0002-add-function-DomainHaveVolatileConstraints.patchDownload
From 9885cda706513ed7d8e85597440609c411678dcc Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Thu, 6 Mar 2025 09:58:56 +0800
Subject: [PATCH v3 2/3] add function DomainHaveVolatileConstraints

bool DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile);

Returns true if the Domain has any constraints.
If you want check this domain have any volatile check constraints,
make sure have_volatile is not NULL.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/utils/cache/typcache.c | 37 ++++++++++++++++++++++++++++++
 src/include/utils/typcache.h       |  1 +
 2 files changed, 38 insertions(+)

diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index 5a3b3788d02..617d0ec27cf 100644
--- a/src/backend/utils/cache/typcache.c
+++ b/src/backend/utils/cache/typcache.c
@@ -1498,6 +1498,43 @@ DomainHasConstraints(Oid type_id)
 }
 
 
+/*
+ * Returns true if the Domain has any constraints.
+ * To check for the presence of volatile constraints, ensure
+ * have_volatile is not NULL. If a volatile constraint exists,
+ * have_volatile will be true.
+ */
+bool
+DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile)
+{
+	TypeCacheEntry *typentry;
+
+	/*
+	 * Note: a side effect is to cause the typcache's domain data to become
+	 * valid.  This is fine since we'll likely need it soon if there is any.
+	 */
+	typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO);
+
+	if (typentry->domainData != NULL)
+	{
+		ListCell   *lc;
+
+		foreach(lc, typentry->domainData->constraints)
+		{
+			DomainConstraintState *r = (DomainConstraintState *) lfirst(lc);
+
+			if (r->constrainttype == DOM_CONSTRAINT_CHECK &&
+				contain_volatile_functions((Node *) r->check_expr))
+			{
+				*have_volatile = true;
+				break;
+			}
+		}
+		return true;
+	}
+	return false;
+}
+
 /*
  * array_element_has_equality and friends are helper routines to check
  * whether we should believe that array_eq and related functions will work
diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h
index 562a581333a..36257c4240c 100644
--- a/src/include/utils/typcache.h
+++ b/src/include/utils/typcache.h
@@ -183,6 +183,7 @@ extern void InitDomainConstraintRef(Oid type_id, DomainConstraintRef *ref,
 extern void UpdateDomainConstraintRef(DomainConstraintRef *ref);
 
 extern bool DomainHasConstraints(Oid type_id);
+extern bool DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile);
 
 extern TupleDesc lookup_rowtype_tupdesc(Oid type_id, int32 typmod);
 
-- 
2.34.1

v3-0003-fast-default-for-domain-with-constraints.patchtext/x-patch; charset=US-ASCII; name=v3-0003-fast-default-for-domain-with-constraints.patchDownload
From c671b17c11f33eebb2d457191c606b992421d6d5 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Thu, 6 Mar 2025 10:44:11 +0800
Subject: [PATCH v3 3/3] fast default for domain with constraints

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

If we evaluate CoerceToDomain to false, in ATExecAddColumn, the defval node
evaluation value cannot be cast to the domain.  However, in some scarenio we
cannot fail at the Phase 2 stage in cases where the table is empty. For example,
the to be added column is domain x and domain x is "check(value > 10) default 8".

Therefore, if an error occurred while evaluation, do not raise the error,
we signal Phase 3 to do table rewrite, error will be raised on Phase 3.

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 need table rewrite for domain with volatile check constraints.
so there will be less surprising behavior.

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           |  53 +++++++++--
 src/test/regress/expected/fast_default.out | 103 +++++++++++++++++++++
 src/test/regress/sql/fast_default.sql      |  71 ++++++++++++++
 3 files changed, 221 insertions(+), 6 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 59156a1c1f6..55d7528dea5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7332,6 +7332,7 @@ 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.
 	 *
+	 * XXXX this para should be removed?
 	 * 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
@@ -7359,6 +7360,8 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		bool		has_domain_constraints;
 		bool		has_missing = false;
+		bool		no_explicit_defval = false;
+		bool		has_volatile = false;
 
 		/*
 		 * For an identity column, we can't use build_column_default(),
@@ -7376,8 +7379,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		else
 			defval = (Expr *) build_column_default(rel, attribute->attnum);
 
+		/*
+		 * if defval is there, atthasdef is false, that means the defval comes
+		 * from domain default value, no explicit DEFAULT expression specified.
+		 * In that case we need evaluate defval error safe way, so
+		 * domain specification such as ``check(value > 10) default 8 ``
+		 * can be added to empty table.
+		*/
+		if (defval)
+		{
+			TupleDesc	rd_att = rel->rd_att;
+			Form_pg_attribute att_tup = TupleDescAttr(rd_att, attribute->attnum - 1);
+			if (!att_tup->atthasdef)
+				no_explicit_defval = true;
+		}
+
 		/* Build CoerceToDomain(NULL) expression if needed */
-		has_domain_constraints = DomainHasConstraints(attribute->atttypid);
+		has_domain_constraints = DomainHaveVolatileConstraints(attribute->atttypid, &has_volatile);
+		if (has_volatile)
+		{
+			Assert(has_domain_constraints);
+			tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+		}
+
 		if (!defval && has_domain_constraints)
 		{
 			Oid			baseTypeId;
@@ -7398,6 +7422,12 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 													-1);
 			if (defval == NULL) /* should not happen */
 				elog(ERROR, "failed to coerce base type to domain");
+			/*
+			 * if domain have not-null constraint or check constraint
+			 * equivalent to not-null, we only want it failure when table have
+			 * some rows. so do it in soft error way.
+			*/
+			no_explicit_defval = true;
 		}
 
 		if (defval)
@@ -7420,13 +7450,14 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			 * 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.
+			 * contain_volatile_functions deems CoerceToDomain immutable.
+			 * We do support soft error evaluation of CoerceToDomain if
+			 * ExprState->escontext is not NULL. In that case if evaluation failed,
+			 * set table rewrite to true, let's fail on Phase 3.
 			 */
 			if (rel->rd_rel->relkind == RELKIND_RELATION &&
 				!colDef->generated &&
-				!has_domain_constraints &&
+				!has_volatile &&
 				!contain_volatile_functions((Node *) defval))
 			{
 				EState	   *estate;
@@ -7436,10 +7467,20 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 				/* Evaluate the default expression */
 				estate = CreateExecutorState();
-				exprState = ExecPrepareExpr(defval, estate);
+				if (!no_explicit_defval)
+					exprState = ExecPrepareExpr(defval, estate);
+				else
+					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..91c94cf1e2d 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -317,11 +317,114 @@ 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 check constraint
+CREATE DOMAIN domain5 AS int check(value > 10) default 8;
+CREATE DOMAIN domain6 as int not null;
+CREATE DOMAIN domain7 as int check(value is not null);
+CREATE DOMAIN domain8 as int check(value > 10) DEFAULT random(min=>10, max=>100);
+CREATE TABLE t3(a int);
+ALTER TABLE t3 ADD COLUMN b domain5 default 1; --error
+ERROR:  value for domain domain5 violates check constraint "domain5_check"
+ALTER TABLE t3 ADD COLUMN b domain6 default NULL; --error
+ERROR:  domain domain6 does not allow null values
+ALTER TABLE t3 ADD COLUMN b domain6 default 11 + NULL; --error
+ERROR:  domain domain6 does not allow null values
+ALTER TABLE t3 ADD COLUMN b domain7 default 11 + NULL; --error
+ERROR:  value for domain domain7 violates check constraint "domain7_check"
+ALTER TABLE t3 ADD COLUMN b domain6; --ok. because t3 is empty
+NOTICE:  rewriting table t3 for reason 2
+ALTER TABLE t3 ADD COLUMN c domain7; --ok. because t3 is empty
+NOTICE:  rewriting table t3 for reason 2
+ALTER TABLE t3 ADD COLUMN d domain5; --ok. because t3 is empty
+NOTICE:  rewriting table t3 for reason 2
+INSERT INTO t3 default values; --error
+ERROR:  domain domain6 does not allow null values
+INSERT INTO t3(a,b,c,d) values(1,2, 3, 12); --ok
+DROP TABLE t3;
+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 domain7; --table rewrite, then fail
+NOTICE:  rewriting table t3 for reason 2
+ERROR:  value for domain domain7 violates check constraint "domain7_check"
+ALTER TABLE t3 ADD COLUMN b domain5 default 12; --no table rewrite
+ALTER TABLE t3 ADD COLUMN c domain6 default 13; --no table rewrite
+ALTER TABLE t3 ADD COLUMN d domain7 default 14; --no table rewrite
+--no table rewrite, we consider explicit column default expression, not the domain default
+ALTER TABLE t3 ADD COLUMN e domain8 default 15;
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+ORDER BY attnum;
+ attnum | attname | atthasmissing | atthasdef | attmissingval 
+--------+---------+---------------+-----------+---------------
+      1 | a       | f             | f         | 
+      2 | b       | t             | t         | {12}
+      3 | c       | t             | t         | {13}
+      4 | d       | t             | t         | {14}
+      5 | e       | t             | t         | {15}
+(5 rows)
+
+--table rewrite. we are applying domain volatile default expresion
+ALTER TABLE t3 ADD COLUMN f domain8;
+NOTICE:  rewriting table t3 for reason 2
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass
+ORDER BY attnum;
+ attnum | attname | atthasmissing | atthasdef | attmissingval 
+--------+---------+---------------+-----------+---------------
+      1 | a       | f             | f         | 
+      2 | b       | f             | t         | 
+      3 | c       | f             | t         | 
+      4 | d       | f             | t         | 
+      5 | e       | f             | t         | 
+      6 | f       | f             | f         | 
+(6 rows)
+
+SELECT a,b,c,d,e,f > 10 as f_ok FROM t3 ORDER BY a;
+ a | b  | c  | d  | e  | f_ok 
+---+----+----+----+----+------
+ 1 | 12 | 13 | 14 | 15 | t
+ 2 | 12 | 13 | 14 | 15 | t
+(2 rows)
+
+------test table rewrite for volatile domain constraints.
+CREATE DOMAIN domain9 as int check((value + random(min=>11::int, max=>11)) > 12); --volatile
+CREATE DOMAIN domain10 as int check((value + random(min=>11::int, max=>11)) > 12) default 1; --volatile
+CREATE TABLE t4(a int);
+INSERT INTO t4 VALUES(1),(2);
+--all these will table rewrite and be ok.
+ALTER TABLE t4 ADD COLUMN b domain9; --default to NULL
+NOTICE:  rewriting table t4 for reason 2
+ALTER TABLE t4 ADD COLUMN c domain9 default 14;
+NOTICE:  rewriting table t4 for reason 2
+SELECT COUNT(*) AS expect_zero
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't4'::regclass AND attmissingval IS NOT NULL;
+ expect_zero 
+-------------
+           0
+(1 row)
+
 DROP TABLE t2;
+DROP TABLE t3;
+DROP TABLE t4;
 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 DOMAIN domain9;
+DROP DOMAIN domain10;
 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..f5f6cdb48b8 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -287,11 +287,82 @@ 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 check constraint
+CREATE DOMAIN domain5 AS int check(value > 10) default 8;
+CREATE DOMAIN domain6 as int not null;
+CREATE DOMAIN domain7 as int check(value is not null);
+CREATE DOMAIN domain8 as int check(value > 10) DEFAULT random(min=>10, max=>100);
+
+CREATE TABLE t3(a int);
+ALTER TABLE t3 ADD COLUMN b domain5 default 1; --error
+ALTER TABLE t3 ADD COLUMN b domain6 default NULL; --error
+ALTER TABLE t3 ADD COLUMN b domain6 default 11 + NULL; --error
+ALTER TABLE t3 ADD COLUMN b domain7 default 11 + NULL; --error
+
+ALTER TABLE t3 ADD COLUMN b domain6; --ok. because t3 is empty
+ALTER TABLE t3 ADD COLUMN c domain7; --ok. because t3 is empty
+ALTER TABLE t3 ADD COLUMN d domain5; --ok. because t3 is empty
+INSERT INTO t3 default values; --error
+INSERT INTO t3(a,b,c,d) values(1,2, 3, 12); --ok
+
+DROP TABLE t3;
+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 domain7; --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
+ALTER TABLE t3 ADD COLUMN d domain7 default 14; --no table rewrite
+--no table rewrite, we consider explicit column default expression, not the domain default
+ALTER TABLE t3 ADD COLUMN e domain8 default 15;
+
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+ORDER BY attnum;
+
+--table rewrite. we are applying domain volatile default expresion
+ALTER TABLE t3 ADD COLUMN f domain8;
+
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass
+ORDER BY attnum;
+
+SELECT a,b,c,d,e,f > 10 as f_ok FROM t3 ORDER BY a;
+
+
+------test table rewrite for volatile domain constraints.
+CREATE DOMAIN domain9 as int check((value + random(min=>11::int, max=>11)) > 12); --volatile
+CREATE DOMAIN domain10 as int check((value + random(min=>11::int, max=>11)) > 12) default 1; --volatile
+
+CREATE TABLE t4(a int);
+INSERT INTO t4 VALUES(1),(2);
+
+--all these will table rewrite and be ok.
+ALTER TABLE t4 ADD COLUMN b domain9; --default to NULL
+ALTER TABLE t4 ADD COLUMN c domain9 default 14;
+
+SELECT COUNT(*) AS expect_zero
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't4'::regclass AND attmissingval IS NOT NULL;
+
 DROP TABLE t2;
+DROP TABLE t3;
+DROP TABLE t4;
 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 DOMAIN domain9;
+DROP DOMAIN domain10;
 DROP FUNCTION foo(INT);
 
 -- Fall back to full rewrite for volatile expressions
-- 
2.34.1

#5jian he
jian.universality@gmail.com
In reply to: jian he (#4)
Re: support fast default for domain with constraints

On Thu, Mar 6, 2025 at 11:04 AM jian he <jian.universality@gmail.com> wrote:

hi.

rearrange the patch.
v3-0001 and v3-0002 is preparare patches.
v3-0001 add function: ExecPrepareExprSafe and ExecInitExprSafe.
v3-0002 add function: DomainHaveVolatileConstraints

i actually do need DomainHaveVolatileConstraints
for virtual generated columns over domain with constraints in [1]/messages/by-id/CACJufxHArQysbDkWFmvK+D1TPHQWWTxWN15cMuUaTYX3xhQXgg@mail.gmail.com,
which I am working on.

for example:
create domain d1 as int check(value > random(min=>11::int, max=>12));
create domain d2 as int check(value > 12);
create table t(a int);
insert into t select g from generate_series(1, 10) g;

----we do need table rewrite in phase 3.
alter table t add column b d1 generated always as (a+11) virtual;

--we can only do table scan in phase 3.
alter table t add column c d2 generated always as (a + 12) virtual;

Generally, table rewrite is more expensive than table scan.
In the above case, if domain constraints are not volatile, table scan
should be fine.

[1]: /messages/by-id/CACJufxHArQysbDkWFmvK+D1TPHQWWTxWN15cMuUaTYX3xhQXgg@mail.gmail.com

#6jian he
jian.universality@gmail.com
In reply to: jian he (#5)
3 attachment(s)
Re: support fast default for domain with constraints

hi.

rebase because of commit: 8dd7c7cd0a2605d5301266a6b67a569d6a305106
also did minor enhancement.

v4-0001 add function: ExecPrepareExprSafe and ExecInitExprSafe.
v4-0002 add function: DomainHaveVolatileConstraints
v4-0003 tests and apply fast default for domain with constraints.

v4-0003 table with empty rows aligned with master behavior.
also will do table rewrite if the new column is domain with volatile
check constraints,
so less surprising behavior.

Attachments:

v4-0001-soft-error-variant-of-ExecPrepareExpr-ExecInitExp.patchtext/x-patch; charset=US-ASCII; name=v4-0001-soft-error-variant-of-ExecPrepareExpr-ExecInitExp.patchDownload
From 09547bbc65f20846ac28efce035c016b21a75825 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 24 Mar 2025 16:07:46 +0800
Subject: [PATCH v4 1/3] soft error variant of ExecPrepareExpr, ExecInitExpr

ExecInitExprSafe: soft error of ExecInitExpr.
ExecPrepareExprSafe: soft error of ExecPrepareExpr.
ExecPrepareExprSafe initialize for expression execution with soft error support.
not all expression node support it. Like node CoerceToDomain support it.

XXX more comments.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/executor/execExpr.c | 63 +++++++++++++++++++++++++++++++++
 src/include/executor/executor.h |  2 ++
 2 files changed, 65 insertions(+)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index f1569879b52..9182ba446a0 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -170,6 +170,47 @@ ExecInitExpr(Expr *node, PlanState *parent)
 	return state;
 }
 
+/*
+ * ExecInitExprSafe: soft error variant of ExecInitExpr.
+ *
+ * use it only for expression nodes support soft errors, not all expression
+ * nodes support it.
+*/
+ExprState *
+ExecInitExprSafe(Expr *node, PlanState *parent)
+{
+	ExprState  *state;
+	ExprEvalStep scratch = {0};
+
+	/* Special case: NULL expression produces a NULL ExprState pointer */
+	if (node == NULL)
+		return NULL;
+
+	/* Initialize ExprState with empty step list */
+	state = makeNode(ExprState);
+	state->expr = node;
+	state->parent = parent;
+	state->ext_params = NULL;
+	state->escontext = makeNode(ErrorSaveContext);
+	state->escontext->type = T_ErrorSaveContext;
+	state->escontext->error_occurred = false;
+	state->escontext->details_wanted = true;
+
+	/* Insert setup steps as needed */
+	ExecCreateExprSetupSteps(state, (Node *) node);
+
+	/* Compile the expression proper */
+	ExecInitExprRec(node, state, &state->resvalue, &state->resnull);
+
+	/* Finally, append a DONE step */
+	scratch.opcode = EEOP_DONE_RETURN;
+	ExprEvalPushStep(state, &scratch);
+
+	ExecReadyExpr(state);
+
+	return state;
+}
+
 /*
  * ExecInitExprWithParams: prepare a standalone expression tree for execution
  *
@@ -778,6 +819,28 @@ ExecPrepareExpr(Expr *node, EState *estate)
 	return result;
 }
 
+/*
+ * ExecPrepareExprSafe: soft error variant of ExecPrepareExpr.
+ *
+ * use it when expression node *support* soft error expression execution.
+ */
+ExprState *
+ExecPrepareExprSafe(Expr *node, EState *estate)
+{
+	ExprState  *result;
+	MemoryContext oldcontext;
+
+	oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
+
+	node = expression_planner(node);
+
+	result = ExecInitExprSafe(node, NULL);
+
+	MemoryContextSwitchTo(oldcontext);
+
+	return result;
+}
+
 /*
  * ExecPrepareQual --- initialize for qual execution outside a normal
  * Plan tree context.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 0db5d18ba22..464c7318de1 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -305,6 +305,7 @@ ExecProcNode(PlanState *node)
  * prototypes from functions in execExpr.c
  */
 extern ExprState *ExecInitExpr(Expr *node, PlanState *parent);
+extern ExprState *ExecInitExprSafe(Expr *node, PlanState *parent);
 extern ExprState *ExecInitExprWithParams(Expr *node, ParamListInfo ext_params);
 extern ExprState *ExecInitQual(List *qual, PlanState *parent);
 extern ExprState *ExecInitCheck(List *qual, PlanState *parent);
@@ -353,6 +354,7 @@ extern ProjectionInfo *ExecBuildUpdateProjection(List *targetList,
 												 TupleTableSlot *slot,
 												 PlanState *parent);
 extern ExprState *ExecPrepareExpr(Expr *node, EState *estate);
+extern ExprState *ExecPrepareExprSafe(Expr *node, EState *estate);
 extern ExprState *ExecPrepareQual(List *qual, EState *estate);
 extern ExprState *ExecPrepareCheck(List *qual, EState *estate);
 extern List *ExecPrepareExprList(List *nodes, EState *estate);
-- 
2.34.1

v4-0003-fast-default-for-domain-with-constraints.patchtext/x-patch; charset=US-ASCII; name=v4-0003-fast-default-for-domain-with-constraints.patchDownload
From b9739edd3de443b203a14db3a6175988125de135 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 24 Mar 2025 19:08:25 +0800
Subject: [PATCH v4 3/3] fast default for domain with constraints

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

If we evaluate CoerceToDomain to false, in ATExecAddColumn, the defval node
evaluation value cannot be cast to the domain.  However, in some scarenio we
cannot fail at the Phase 2 stage in cases where the table is empty. For example,
the to be added column is type of domain x, domain x is "check(value > 10) default 8".

Therefore, if an error occurred while evaluation, do not raise the error,
we signal Phase 3 to do table rewrite, error will be raised on Phase 3.

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 need table rewrite for domain with volatile check constraints.
so there will be less surprising behavior.

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           |  66 ++++++++++---
 src/test/regress/expected/fast_default.out | 103 +++++++++++++++++++++
 src/test/regress/sql/fast_default.sql      |  68 ++++++++++++++
 3 files changed, 222 insertions(+), 15 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1202544ebd0..abddb9cdba7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7342,15 +7342,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.
@@ -7369,6 +7360,8 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		bool		has_domain_constraints;
 		bool		has_missing = false;
+		bool		explicit_defval = true;
+		bool		has_volatile = false;
 
 		/*
 		 * For an identity column, we can't use build_column_default(),
@@ -7386,8 +7379,31 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		else
 			defval = (Expr *) build_column_default(rel, attribute->attnum);
 
+		/*
+		 * if defval is there, atthasdef is false, that means the defval comes
+		 * from domain default expression and no explicit DEFAULT expression has
+		 * been specified.
+		 * In that case we need evaluate defval error safe way, so new column
+		 * with domain specification such as ``CHECK(VALUE > 10) DEFAULT 8 ``
+		 * can be addded to empty table.
+		*/
+		if (defval)
+		{
+			TupleDesc	rd_att = rel->rd_att;
+			Form_pg_attribute att_tup = TupleDescAttr(rd_att, attribute->attnum - 1);
+			if (!att_tup->atthasdef)
+				explicit_defval = false;
+		}
+
+		has_domain_constraints = DomainHaveVolatileConstraints(attribute->atttypid, &has_volatile);
+		/* new column with volatile domain constraint, then 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;
@@ -7408,6 +7424,13 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 													-1);
 			if (defval == NULL) /* should not happen */
 				elog(ERROR, "failed to coerce base type to domain");
+
+			/*
+			 * if domain have not-null constraint or check constraint that is
+			 * equivalent to not-null, we only want it failure when table have
+			 * some rows.  let's evaulate defval in a soft error way.
+			*/
+			explicit_defval = false;
 		}
 
 		if (defval)
@@ -7430,13 +7453,16 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			 * 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.
+			 * contain_volatile_functions deems CoerceToDomain immutable.
+			 * But domain with volatile constraint will need table rewrite,
+			 * regardless of domain's default expression.
+			 * We do support soft error evaluation of CoerceToDomain if
+			 * ExprState->escontext is not NULL. In that case if evaluation
+			 * failed, set table rewrite to true, let's fail on Phase 3.
 			 */
 			if (rel->rd_rel->relkind == RELKIND_RELATION &&
 				!colDef->generated &&
-				!has_domain_constraints &&
+				!has_volatile &&
 				!contain_volatile_functions((Node *) defval))
 			{
 				EState	   *estate;
@@ -7446,10 +7472,20 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 				/* Evaluate the default expression */
 				estate = CreateExecutorState();
-				exprState = ExecPrepareExpr(defval, estate);
+				if (explicit_defval)
+					exprState = ExecPrepareExpr(defval, estate);
+				else
+					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..73fb23fd79b 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -317,11 +317,114 @@ 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 check constraint
+CREATE DOMAIN domain5 AS int check(value > 10) default 8;
+CREATE DOMAIN domain6 as int not null;
+CREATE DOMAIN domain7 as int check(value is not null);
+CREATE DOMAIN domain8 as int check(value > 10) DEFAULT random(min=>10, max=>100);
+CREATE TABLE t3(a int);
+ALTER TABLE t3 ADD COLUMN b domain5 default 1; --error
+ERROR:  value for domain domain5 violates check constraint "domain5_check"
+ALTER TABLE t3 ADD COLUMN b domain6 default NULL; --error
+ERROR:  domain domain6 does not allow null values
+ALTER TABLE t3 ADD COLUMN b domain6 default 11 + NULL; --error
+ERROR:  domain domain6 does not allow null values
+ALTER TABLE t3 ADD COLUMN b domain7 default 11 + NULL; --error
+ERROR:  value for domain domain7 violates check constraint "domain7_check"
+ALTER TABLE t3 ADD COLUMN d domain5; --ok. because t3 is empty
+NOTICE:  rewriting table t3 for reason 2
+ALTER TABLE t3 ADD COLUMN b domain6; --ok. because t3 is empty
+NOTICE:  rewriting table t3 for reason 2
+ALTER TABLE t3 ADD COLUMN c domain7; --ok. because t3 is empty
+NOTICE:  rewriting table t3 for reason 2
+INSERT INTO t3 default values; --error
+ERROR:  value for domain domain5 violates check constraint "domain5_check"
+INSERT INTO t3(a,b,c,d) values(1,2, 3, 12); --ok
+DROP TABLE t3;
+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 domain7; --table rewrite, then fail
+NOTICE:  rewriting table t3 for reason 2
+ERROR:  value for domain domain7 violates check constraint "domain7_check"
+ALTER TABLE t3 ADD COLUMN b domain5 default 12; --no table rewrite
+ALTER TABLE t3 ADD COLUMN c domain6 default 13; --no table rewrite
+ALTER TABLE t3 ADD COLUMN d domain7 default 14; --no table rewrite
+--no table rewrite. explicit column default expression override domain default expression
+ALTER TABLE t3 ADD COLUMN e domain8 default 15;
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+ORDER BY attnum;
+ attnum | attname | atthasmissing | atthasdef | attmissingval 
+--------+---------+---------------+-----------+---------------
+      1 | a       | f             | f         | 
+      2 | b       | t             | t         | {12}
+      3 | c       | t             | t         | {13}
+      4 | d       | t             | t         | {14}
+      5 | e       | t             | t         | {15}
+(5 rows)
+
+--table rewrite. we are applying domain volatile default expresion
+ALTER TABLE t3 ADD COLUMN f domain8;
+NOTICE:  rewriting table t3 for reason 2
+SELECT  attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM    pg_attribute
+WHERE   attnum > 0 AND attrelid = 't3'::regclass and not attisdropped
+ORDER BY attnum;
+ attnum | attname | atthasmissing | atthasdef | attmissingval 
+--------+---------+---------------+-----------+---------------
+      1 | a       | f             | f         | 
+      2 | b       | f             | t         | 
+      3 | c       | f             | t         | 
+      4 | d       | f             | t         | 
+      5 | e       | f             | t         | 
+      6 | f       | f             | f         | 
+(6 rows)
+
+SELECT a,b,c,d,e,f > 10 as f_ok FROM t3 ORDER BY a;
+ a | b  | c  | d  | e  | f_ok 
+---+----+----+----+----+------
+ 1 | 12 | 13 | 14 | 15 | t
+ 2 | 12 | 13 | 14 | 15 | t
+(2 rows)
+
+------test table rewrite for volatile domain constraints.
+CREATE DOMAIN domain9 as int check((value + random(min=>11::int, max=>11)) > 12); --volatile
+CREATE DOMAIN domain10 as int check((value + random(min=>11::int, max=>11)) > 12) default 1; --volatile
+CREATE TABLE t4(a int);
+INSERT INTO t4 VALUES(1),(2);
+--all these will table rewrite and be ok.
+ALTER TABLE t4 ADD COLUMN b domain9; --default to NULL
+NOTICE:  rewriting table t4 for reason 2
+ALTER TABLE t4 ADD COLUMN c domain10 default 14;
+NOTICE:  rewriting table t4 for reason 2
+SELECT COUNT(*) AS expect_zero
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't4'::regclass AND attmissingval IS NOT NULL;
+ expect_zero 
+-------------
+           0
+(1 row)
+
 DROP TABLE t2;
+DROP TABLE t3;
+DROP TABLE t4;
 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 DOMAIN domain9;
+DROP DOMAIN domain10;
 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..e9187cd2198 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -287,11 +287,79 @@ 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 check constraint
+CREATE DOMAIN domain5 AS int check(value > 10) default 8;
+CREATE DOMAIN domain6 as int not null;
+CREATE DOMAIN domain7 as int check(value is not null);
+CREATE DOMAIN domain8 as int check(value > 10) DEFAULT random(min=>10, max=>100);
+
+CREATE TABLE t3(a int);
+ALTER TABLE t3 ADD COLUMN b domain5 default 1; --error
+ALTER TABLE t3 ADD COLUMN b domain6 default NULL; --error
+ALTER TABLE t3 ADD COLUMN b domain6 default 11 + NULL; --error
+ALTER TABLE t3 ADD COLUMN b domain7 default 11 + NULL; --error
+
+ALTER TABLE t3 ADD COLUMN d domain5; --ok. because t3 is empty
+ALTER TABLE t3 ADD COLUMN b domain6; --ok. because t3 is empty
+ALTER TABLE t3 ADD COLUMN c domain7; --ok. because t3 is empty
+INSERT INTO t3 default values; --error
+INSERT INTO t3(a,b,c,d) values(1,2, 3, 12); --ok
+
+DROP TABLE t3;
+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 domain7; --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
+ALTER TABLE t3 ADD COLUMN d domain7 default 14; --no table rewrite
+--no table rewrite. explicit column default expression override domain default expression
+ALTER TABLE t3 ADD COLUMN e domain8 default 15;
+
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't3'::regclass AND attisdropped is false
+ORDER BY attnum;
+
+--table rewrite. we are applying domain volatile default expresion
+ALTER TABLE t3 ADD COLUMN f domain8;
+
+SELECT  attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM    pg_attribute
+WHERE   attnum > 0 AND attrelid = 't3'::regclass and not attisdropped
+ORDER BY attnum;
+
+SELECT a,b,c,d,e,f > 10 as f_ok FROM t3 ORDER BY a;
+------test table rewrite for volatile domain constraints.
+CREATE DOMAIN domain9 as int check((value + random(min=>11::int, max=>11)) > 12); --volatile
+CREATE DOMAIN domain10 as int check((value + random(min=>11::int, max=>11)) > 12) default 1; --volatile
+
+CREATE TABLE t4(a int);
+INSERT INTO t4 VALUES(1),(2);
+--all these will table rewrite and be ok.
+ALTER TABLE t4 ADD COLUMN b domain9; --default to NULL
+ALTER TABLE t4 ADD COLUMN c domain10 default 14;
+
+SELECT COUNT(*) AS expect_zero
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 't4'::regclass AND attmissingval IS NOT NULL;
+
 DROP TABLE t2;
+DROP TABLE t3;
+DROP TABLE t4;
 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 DOMAIN domain9;
+DROP DOMAIN domain10;
 DROP FUNCTION foo(INT);
 
 -- Fall back to full rewrite for volatile expressions
-- 
2.34.1

v4-0002-add-function-DomainHaveVolatileConstraints.patchtext/x-patch; charset=US-ASCII; name=v4-0002-add-function-DomainHaveVolatileConstraints.patchDownload
From bca9beb0d2da988aec25fe776ea1bfda84897d9a Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Thu, 6 Mar 2025 09:58:56 +0800
Subject: [PATCH v4 2/3] add function DomainHaveVolatileConstraints

bool DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile);

Returns true if the Domain has any constraints.
If you want check this domain have any volatile check constraints,
make sure have_volatile is not NULL.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/utils/cache/typcache.c | 37 ++++++++++++++++++++++++++++++
 src/include/utils/typcache.h       |  1 +
 2 files changed, 38 insertions(+)

diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index 5a3b3788d02..617d0ec27cf 100644
--- a/src/backend/utils/cache/typcache.c
+++ b/src/backend/utils/cache/typcache.c
@@ -1498,6 +1498,43 @@ DomainHasConstraints(Oid type_id)
 }
 
 
+/*
+ * Returns true if the Domain has any constraints.
+ * To check for the presence of volatile constraints, ensure
+ * have_volatile is not NULL. If a volatile constraint exists,
+ * have_volatile will be true.
+ */
+bool
+DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile)
+{
+	TypeCacheEntry *typentry;
+
+	/*
+	 * Note: a side effect is to cause the typcache's domain data to become
+	 * valid.  This is fine since we'll likely need it soon if there is any.
+	 */
+	typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO);
+
+	if (typentry->domainData != NULL)
+	{
+		ListCell   *lc;
+
+		foreach(lc, typentry->domainData->constraints)
+		{
+			DomainConstraintState *r = (DomainConstraintState *) lfirst(lc);
+
+			if (r->constrainttype == DOM_CONSTRAINT_CHECK &&
+				contain_volatile_functions((Node *) r->check_expr))
+			{
+				*have_volatile = true;
+				break;
+			}
+		}
+		return true;
+	}
+	return false;
+}
+
 /*
  * array_element_has_equality and friends are helper routines to check
  * whether we should believe that array_eq and related functions will work
diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h
index 562a581333a..36257c4240c 100644
--- a/src/include/utils/typcache.h
+++ b/src/include/utils/typcache.h
@@ -183,6 +183,7 @@ extern void InitDomainConstraintRef(Oid type_id, DomainConstraintRef *ref,
 extern void UpdateDomainConstraintRef(DomainConstraintRef *ref);
 
 extern bool DomainHasConstraints(Oid type_id);
+extern bool DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile);
 
 extern TupleDesc lookup_rowtype_tupdesc(Oid type_id, int32 typmod);
 
-- 
2.34.1

#7jian he
jian.universality@gmail.com
In reply to: jian he (#6)
3 attachment(s)
Re: support fast default for domain with constraints

On Mon, Mar 24, 2025 at 7:14 PM jian he <jian.universality@gmail.com> wrote:

v4-0003 table with empty rows aligned with master behavior.
also will do table rewrite if the new column is domain with volatile
check constraints,
so less surprising behavior.

I found out that my v4-0003 is wrong.

For example, the following ALTER TABLE ADD COLUMN should not fail.
CREATE DOMAIN domain5 AS int check(value > 10) default 8;
CREATE TABLE t3(a int);
ALTER TABLE t3 ADD COLUMN b domain5 default 1; --ok, table rewrite

I also reduced the bloated tests.
summary of the behavior that is different from master:
if domain constraint is not volatile *and* domain's default expression satisfy
constraint's condition then no need table rewrite.

Attachments:

v5-0002-add-function-DomainHaveVolatileConstraints.patchtext/x-patch; charset=US-ASCII; name=v5-0002-add-function-DomainHaveVolatileConstraints.patchDownload
From 8c3760aa64773412b5127347cc56dccd0042b592 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 19 May 2025 11:11:01 +0800
Subject: [PATCH v5 2/3] add function DomainHaveVolatileConstraints

the signature is:
extern bool DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile);

Returns true if the Domain has any constraints.  If you want check this domain
have any volatile check constraints, make sure have_volatile is not NULL.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/utils/cache/typcache.c | 40 ++++++++++++++++++++++++++++++
 src/include/utils/typcache.h       |  1 +
 2 files changed, 41 insertions(+)

diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index f9aec38a11f..83f195d09d9 100644
--- a/src/backend/utils/cache/typcache.c
+++ b/src/backend/utils/cache/typcache.c
@@ -1500,6 +1500,46 @@ DomainHasConstraints(Oid type_id)
 }
 
 
+/*
+ * Check whether a domain has any constraints, and determine if any of those
+ * constraints contain volatile expressions.
+ *
+ * To detect volatile expressions within domain check constraints, ensure that
+ * have_volatile is not NULL. If have_volatile is NULL, the behavior is
+ * equivalent to that of DomainHasConstraints.
+ */
+bool
+DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile)
+{
+	TypeCacheEntry *typentry;
+
+	/*
+	 * Note: a side effect is to cause the typcache's domain data to become
+	 * valid.  This is fine since we'll likely need it soon if there is any.
+	 */
+	typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO);
+
+	if (typentry->domainData != NULL)
+	{
+		ListCell   *lc;
+
+		foreach(lc, typentry->domainData->constraints)
+		{
+			DomainConstraintState *r = (DomainConstraintState *) lfirst(lc);
+
+			if (r->constrainttype == DOM_CONSTRAINT_CHECK &&
+				contain_volatile_functions((Node *) r->check_expr))
+			{
+				if (have_volatile)
+					*have_volatile = true;
+				break;
+			}
+		}
+		return true;
+	}
+	return false;
+}
+
 /*
  * array_element_has_equality and friends are helper routines to check
  * whether we should believe that array_eq and related functions will work
diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h
index 1cb30f1818c..aa1c86e35c3 100644
--- a/src/include/utils/typcache.h
+++ b/src/include/utils/typcache.h
@@ -184,6 +184,7 @@ extern void InitDomainConstraintRef(Oid type_id, DomainConstraintRef *ref,
 extern void UpdateDomainConstraintRef(DomainConstraintRef *ref);
 
 extern bool DomainHasConstraints(Oid type_id);
+extern bool DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile);
 
 extern TupleDesc lookup_rowtype_tupdesc(Oid type_id, int32 typmod);
 
-- 
2.34.1

v5-0003-fast-default-for-domain-with-constraints.patchtext/x-patch; charset=UTF-8; name=v5-0003-fast-default-for-domain-with-constraints.patchDownload
From 003e9b6b6e3429de0910d01ff6840f4bf0f362c8 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 19 May 2025 14:14:28 +0800
Subject: [PATCH v5 3/3] fast default for domain with constraints
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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           | 45 +++++++++++-------
 src/test/regress/expected/fast_default.out | 55 ++++++++++++++++++++++
 src/test/regress/sql/fast_default.sql      | 44 +++++++++++++++++
 3 files changed, 128 insertions(+), 16 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 54ad38247aa..dba2ea77cad 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,17 @@ 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 +7517,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 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 +7538,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..861208a269a 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -317,11 +317,66 @@ 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=>10, max=>100);
+CREATE DOMAIN domain8 as int check((value + random(min=>11::int, max=>11)) > 12);
+--tests with empty table.
+CREATE TABLE t3(a int);
+ALTER TABLE t3 ADD COLUMN b domain5 default 1; --ok, table rewrite
+NOTICE:  rewriting table t3 for reason 2
+ALTER TABLE t3 ADD COLUMN c domain6 default 11 + NULL; --ok, table rewrite
+NOTICE:  rewriting table t3 for reason 2
+ALTER TABLE t3 ADD COLUMN d domain7 default 2; --ok, table rewrite
+NOTICE:  rewriting table t3 for reason 2
+ALTER TABLE t3 ADD COLUMN e domain8 default 3; --ok, table rewrite
+NOTICE:  rewriting table t3 for reason 2
+DROP TABLE t3;
+--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
+--no table rewrite. explicit column default expression override domain default
+--expression
+ALTER TABLE t3 ADD COLUMN d domain7 default 15;
+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         | {15}
+(3 rows)
+
+--table rewrite. we are applying domain volatile default expresion
+ALTER TABLE t3 ADD COLUMN e domain7;
+NOTICE:  rewriting table t3 for reason 2
+--table rewrite for volatile domain constraints.
+ALTER TABLE t3 ADD COLUMN f domain8 default 14; --table rewrite
+NOTICE:  rewriting table t3 for reason 2
+ALTER TABLE t3 ADD COLUMN f1 domain8; --table rewrite
+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..a98d86a6204 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -287,11 +287,55 @@ 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=>10, max=>100);
+CREATE DOMAIN domain8 as int check((value + random(min=>11::int, max=>11)) > 12);
+
+--tests with empty table.
+CREATE TABLE t3(a int);
+ALTER TABLE t3 ADD COLUMN b domain5 default 1; --ok, table rewrite
+ALTER TABLE t3 ADD COLUMN c domain6 default 11 + NULL; --ok, table rewrite
+ALTER TABLE t3 ADD COLUMN d domain7 default 2; --ok, table rewrite
+ALTER TABLE t3 ADD COLUMN e domain8 default 3; --ok, table rewrite
+DROP TABLE t3;
+
+--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
+--no table rewrite. explicit column default expression override domain default
+--expression
+ALTER TABLE t3 ADD COLUMN d domain7 default 15;
+
+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;
+
+--table rewrite. we are applying domain volatile default expresion
+ALTER TABLE t3 ADD COLUMN e domain7;
+
+--table rewrite for volatile domain constraints.
+ALTER TABLE t3 ADD COLUMN f domain8 default 14; --table rewrite
+ALTER TABLE t3 ADD COLUMN f1 domain8; --table rewrite
+
 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

v5-0001-soft-error-variant-of-ExecPrepareExpr-ExecInitExp.patchtext/x-patch; charset=US-ASCII; name=v5-0001-soft-error-variant-of-ExecPrepareExpr-ExecInitExp.patchDownload
From 42fd50504556d2528f46a19d7c90e6b230986834 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 24 Mar 2025 16:07:46 +0800
Subject: [PATCH v5 1/3] soft error variant of ExecPrepareExpr, ExecInitExpr

ExecInitExprSafe: soft error of ExecInitExpr.
ExecPrepareExprSafe: soft error of ExecPrepareExpr.
ExecPrepareExprSafe initialize for expression execution with soft error support.
not all expression node support it. Like node CoerceToDomain support it.

XXX more comments.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/executor/execExpr.c | 63 +++++++++++++++++++++++++++++++++
 src/include/executor/executor.h |  2 ++
 2 files changed, 65 insertions(+)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index f1569879b52..9182ba446a0 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -170,6 +170,47 @@ ExecInitExpr(Expr *node, PlanState *parent)
 	return state;
 }
 
+/*
+ * ExecInitExprSafe: soft error variant of ExecInitExpr.
+ *
+ * use it only for expression nodes support soft errors, not all expression
+ * nodes support it.
+*/
+ExprState *
+ExecInitExprSafe(Expr *node, PlanState *parent)
+{
+	ExprState  *state;
+	ExprEvalStep scratch = {0};
+
+	/* Special case: NULL expression produces a NULL ExprState pointer */
+	if (node == NULL)
+		return NULL;
+
+	/* Initialize ExprState with empty step list */
+	state = makeNode(ExprState);
+	state->expr = node;
+	state->parent = parent;
+	state->ext_params = NULL;
+	state->escontext = makeNode(ErrorSaveContext);
+	state->escontext->type = T_ErrorSaveContext;
+	state->escontext->error_occurred = false;
+	state->escontext->details_wanted = true;
+
+	/* Insert setup steps as needed */
+	ExecCreateExprSetupSteps(state, (Node *) node);
+
+	/* Compile the expression proper */
+	ExecInitExprRec(node, state, &state->resvalue, &state->resnull);
+
+	/* Finally, append a DONE step */
+	scratch.opcode = EEOP_DONE_RETURN;
+	ExprEvalPushStep(state, &scratch);
+
+	ExecReadyExpr(state);
+
+	return state;
+}
+
 /*
  * ExecInitExprWithParams: prepare a standalone expression tree for execution
  *
@@ -778,6 +819,28 @@ ExecPrepareExpr(Expr *node, EState *estate)
 	return result;
 }
 
+/*
+ * ExecPrepareExprSafe: soft error variant of ExecPrepareExpr.
+ *
+ * use it when expression node *support* soft error expression execution.
+ */
+ExprState *
+ExecPrepareExprSafe(Expr *node, EState *estate)
+{
+	ExprState  *result;
+	MemoryContext oldcontext;
+
+	oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
+
+	node = expression_planner(node);
+
+	result = ExecInitExprSafe(node, NULL);
+
+	MemoryContextSwitchTo(oldcontext);
+
+	return result;
+}
+
 /*
  * ExecPrepareQual --- initialize for qual execution outside a normal
  * Plan tree context.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index ae99407db89..a26160042ee 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -346,6 +346,7 @@ ExecProcNode(PlanState *node)
  * prototypes from functions in execExpr.c
  */
 extern ExprState *ExecInitExpr(Expr *node, PlanState *parent);
+extern ExprState *ExecInitExprSafe(Expr *node, PlanState *parent);
 extern ExprState *ExecInitExprWithParams(Expr *node, ParamListInfo ext_params);
 extern ExprState *ExecInitQual(List *qual, PlanState *parent);
 extern ExprState *ExecInitCheck(List *qual, PlanState *parent);
@@ -394,6 +395,7 @@ extern ProjectionInfo *ExecBuildUpdateProjection(List *targetList,
 												 TupleTableSlot *slot,
 												 PlanState *parent);
 extern ExprState *ExecPrepareExpr(Expr *node, EState *estate);
+extern ExprState *ExecPrepareExprSafe(Expr *node, EState *estate);
 extern ExprState *ExecPrepareQual(List *qual, EState *estate);
 extern ExprState *ExecPrepareCheck(List *qual, EState *estate);
 extern List *ExecPrepareExprList(List *nodes, EState *estate);
-- 
2.34.1

#8jian he
jian.universality@gmail.com
In reply to: jian he (#7)
3 attachment(s)
Re: support fast default for domain with constraints

Attachments:

v6-0001-soft-error-variant-of-ExecPrepareExpr-ExecInitExpr.patchapplication/x-patch; name=v6-0001-soft-error-variant-of-ExecPrepareExpr-ExecInitExpr.patchDownload
From 1df9fc6e8e645463e864f44492d532def74c8437 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 24 Mar 2025 16:07:46 +0800
Subject: [PATCH v6 1/3] soft error variant of ExecPrepareExpr, ExecInitExpr

ExecInitExprSafe: soft error of ExecInitExpr.
ExecPrepareExprSafe: soft error of ExecPrepareExpr.
ExecPrepareExprSafe initialize for expression execution with soft error support.
not all expression node support it. Like node CoerceToDomain support it.

XXX more comments.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/executor/execExpr.c | 63 +++++++++++++++++++++++++++++++++
 src/include/executor/executor.h |  2 ++
 2 files changed, 65 insertions(+)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index f1569879b52..9182ba446a0 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -170,6 +170,47 @@ ExecInitExpr(Expr *node, PlanState *parent)
 	return state;
 }
 
+/*
+ * ExecInitExprSafe: soft error variant of ExecInitExpr.
+ *
+ * use it only for expression nodes support soft errors, not all expression
+ * nodes support it.
+*/
+ExprState *
+ExecInitExprSafe(Expr *node, PlanState *parent)
+{
+	ExprState  *state;
+	ExprEvalStep scratch = {0};
+
+	/* Special case: NULL expression produces a NULL ExprState pointer */
+	if (node == NULL)
+		return NULL;
+
+	/* Initialize ExprState with empty step list */
+	state = makeNode(ExprState);
+	state->expr = node;
+	state->parent = parent;
+	state->ext_params = NULL;
+	state->escontext = makeNode(ErrorSaveContext);
+	state->escontext->type = T_ErrorSaveContext;
+	state->escontext->error_occurred = false;
+	state->escontext->details_wanted = true;
+
+	/* Insert setup steps as needed */
+	ExecCreateExprSetupSteps(state, (Node *) node);
+
+	/* Compile the expression proper */
+	ExecInitExprRec(node, state, &state->resvalue, &state->resnull);
+
+	/* Finally, append a DONE step */
+	scratch.opcode = EEOP_DONE_RETURN;
+	ExprEvalPushStep(state, &scratch);
+
+	ExecReadyExpr(state);
+
+	return state;
+}
+
 /*
  * ExecInitExprWithParams: prepare a standalone expression tree for execution
  *
@@ -778,6 +819,28 @@ ExecPrepareExpr(Expr *node, EState *estate)
 	return result;
 }
 
+/*
+ * ExecPrepareExprSafe: soft error variant of ExecPrepareExpr.
+ *
+ * use it when expression node *support* soft error expression execution.
+ */
+ExprState *
+ExecPrepareExprSafe(Expr *node, EState *estate)
+{
+	ExprState  *result;
+	MemoryContext oldcontext;
+
+	oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
+
+	node = expression_planner(node);
+
+	result = ExecInitExprSafe(node, NULL);
+
+	MemoryContextSwitchTo(oldcontext);
+
+	return result;
+}
+
 /*
  * ExecPrepareQual --- initialize for qual execution outside a normal
  * Plan tree context.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 104b059544d..1c4e4a8eebb 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -318,6 +318,7 @@ ExecProcNode(PlanState *node)
  * prototypes from functions in execExpr.c
  */
 extern ExprState *ExecInitExpr(Expr *node, PlanState *parent);
+extern ExprState *ExecInitExprSafe(Expr *node, PlanState *parent);
 extern ExprState *ExecInitExprWithParams(Expr *node, ParamListInfo ext_params);
 extern ExprState *ExecInitQual(List *qual, PlanState *parent);
 extern ExprState *ExecInitCheck(List *qual, PlanState *parent);
@@ -366,6 +367,7 @@ extern ProjectionInfo *ExecBuildUpdateProjection(List *targetList,
 												 TupleTableSlot *slot,
 												 PlanState *parent);
 extern ExprState *ExecPrepareExpr(Expr *node, EState *estate);
+extern ExprState *ExecPrepareExprSafe(Expr *node, EState *estate);
 extern ExprState *ExecPrepareQual(List *qual, EState *estate);
 extern ExprState *ExecPrepareCheck(List *qual, EState *estate);
 extern List *ExecPrepareExprList(List *nodes, EState *estate);
-- 
2.34.1

v6-0003-fast-default-for-domain-with-constraints.patchapplication/x-patch; name=v6-0003-fast-default-for-domain-with-constraints.patchDownload
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

v6-0002-add-function-DomainHaveVolatileConstraints.patchapplication/x-patch; name=v6-0002-add-function-DomainHaveVolatileConstraints.patchDownload
From d476f84ca48da396526cd15349b5d8ea93078cdf Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 19 May 2025 11:11:01 +0800
Subject: [PATCH v6 2/3] add function DomainHaveVolatileConstraints

the signature is:
extern bool DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile);

Returns true if the Domain has any constraints.  If you want check this domain
have any volatile check constraints, make sure have_volatile is not NULL.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/utils/cache/typcache.c | 40 ++++++++++++++++++++++++++++++
 src/include/utils/typcache.h       |  1 +
 2 files changed, 41 insertions(+)

diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index f9aec38a11f..83f195d09d9 100644
--- a/src/backend/utils/cache/typcache.c
+++ b/src/backend/utils/cache/typcache.c
@@ -1500,6 +1500,46 @@ DomainHasConstraints(Oid type_id)
 }
 
 
+/*
+ * Check whether a domain has any constraints, and determine if any of those
+ * constraints contain volatile expressions.
+ *
+ * To detect volatile expressions within domain check constraints, ensure that
+ * have_volatile is not NULL. If have_volatile is NULL, the behavior is
+ * equivalent to that of DomainHasConstraints.
+ */
+bool
+DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile)
+{
+	TypeCacheEntry *typentry;
+
+	/*
+	 * Note: a side effect is to cause the typcache's domain data to become
+	 * valid.  This is fine since we'll likely need it soon if there is any.
+	 */
+	typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO);
+
+	if (typentry->domainData != NULL)
+	{
+		ListCell   *lc;
+
+		foreach(lc, typentry->domainData->constraints)
+		{
+			DomainConstraintState *r = (DomainConstraintState *) lfirst(lc);
+
+			if (r->constrainttype == DOM_CONSTRAINT_CHECK &&
+				contain_volatile_functions((Node *) r->check_expr))
+			{
+				if (have_volatile)
+					*have_volatile = true;
+				break;
+			}
+		}
+		return true;
+	}
+	return false;
+}
+
 /*
  * array_element_has_equality and friends are helper routines to check
  * whether we should believe that array_eq and related functions will work
diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h
index 1cb30f1818c..aa1c86e35c3 100644
--- a/src/include/utils/typcache.h
+++ b/src/include/utils/typcache.h
@@ -184,6 +184,7 @@ extern void InitDomainConstraintRef(Oid type_id, DomainConstraintRef *ref,
 extern void UpdateDomainConstraintRef(DomainConstraintRef *ref);
 
 extern bool DomainHasConstraints(Oid type_id);
+extern bool DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile);
 
 extern TupleDesc lookup_rowtype_tupdesc(Oid type_id, int32 typmod);
 
-- 
2.34.1

#9jian he
jian.universality@gmail.com
In reply to: jian he (#8)
4 attachment(s)
Re: support fast default for domain with constraints

hi.

in previous patches v6-0001 to v6-0003, we added support for ALTER TABLE ADD
COLUMN with fast defaults for domains having non-volatile constraints.

inspired by another patch of mine: https://commitfest.postgresql.org/patch/5907
I believe it's doable to perform only a table scan when using ALTER TABLE ADD
COLUMN with a domain that has volatile constraints.

some example:
CREATE DOMAIN domain8 as int check((value + random(min=>11::int,
max=>11)) > 12);
CREATE TABLE t3(a int);
INSERT INTO t3 VALUES(1),(2);
ALTER TABLE t3 ADD COLUMN f domain8 default 1; --error while coercing to domain
ALTER TABLE t3 ADD COLUMN f domain8 default 20; --ok

The idea is the same as mentioned in [1]/messages/by-id/CACJufxFhWyWzf2sJS9txSKeyA8hstxGDb8q2QWWwbo5Q1smPMA@mail.gmail.com,
for struct NewColumnValue, add another field (scan_only) to indicate
that we use table scan to evaluate the CoerceToDomain node.

summary of the attached v7.
v7-0001, v7-00002: preparatory patch.
v7-0003 adds fast default support for ALTER TABLE ADD COLUMN when the domain has
non-volatile constraints.
A table rewrite is still required for domains with volatile constraints.

v7-0004 skip table rewrite (table scan only) for ALTER TABLE ADD
COLUMN with domains has volatile constraints.

[1]: /messages/by-id/CACJufxFhWyWzf2sJS9txSKeyA8hstxGDb8q2QWWwbo5Q1smPMA@mail.gmail.com

Attachments:

v7-0003-fast-default-for-domain-with-constraints.patchtext/x-patch; charset=US-ASCII; name=v7-0003-fast-default-for-domain-with-constraints.patchDownload
From 78cf6b0d9013267ad7d8e2e9a1ac2fac564df077 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 v7 3/4] 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 082a3575d62..c58094a39c0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7428,15 +7428,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.
@@ -7455,6 +7446,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(),
@@ -7472,8 +7464,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;
@@ -7515,14 +7517,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;
@@ -7532,10 +7538,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

v7-0004-table-scan-only-when-adding-domain-with-volatile-constraints.patchtext/x-patch; charset=US-ASCII; name=v7-0004-table-scan-only-when-adding-domain-with-volatile-constraints.patchDownload
From b2c056c2e0d5d7ca52bcb859beb2416f8802cb21 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 1 Sep 2025 13:47:47 +0800
Subject: [PATCH v7 4/4] table scan only when adding domain with volatile
 constraints

When adding a new column, a table scan is sufficient to evaluate domains with
volatile check constraints.

example demo:
CREATE DOMAIN domain8 as int check((value + random(min=>11::int, max=>11)) > 12);
CREATE TABLE t3(a int);
INSERT INTO t3 VALUES(1),(2);

ALTER TABLE t3 ADD COLUMN f domain8 default 1; --error while coercing to domain
ALTER TABLE t3 ADD COLUMN f domain8 default 20; --ok

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/commands/tablecmds.c           | 72 ++++++++++++++++++----
 src/test/regress/expected/fast_default.out | 27 +++++++-
 src/test/regress/sql/fast_default.sql      | 15 ++++-
 3 files changed, 98 insertions(+), 16 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c58094a39c0..fcb378e79d5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -230,6 +230,9 @@ typedef struct NewConstraint
  * are just copied from the old table during ATRewriteTable.  Note that the
  * expr is an expression over *old* table values, except when is_generated
  * is true; then it is an expression over columns of the *new* tuple.
+ *
+ * If scan_only is true, it means in phase3, table scan is enough to evaulate expr.
+ * Currently, this is supported when expr is CoerceToDomain.
  */
 typedef struct NewColumnValue
 {
@@ -237,6 +240,8 @@ typedef struct NewColumnValue
 	Expr	   *expr;			/* expression to compute */
 	ExprState  *exprstate;		/* execution state */
 	bool		is_generated;	/* is it a GENERATED expression? */
+	bool		scan_only;		/* use table scan to evaulate expression,
+								 * useful only when table rewrite is false */
 } NewColumnValue;
 
 /*
@@ -6008,7 +6013,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * rebuild data.
 			 */
 			if (tab->constraints != NIL || tab->verify_new_notnull ||
-				tab->partition_constraint != NULL)
+				tab->partition_constraint != NULL ||
+				tab->newvals)
 				ATRewriteTable(tab, InvalidOid);
 
 			/*
@@ -6118,7 +6124,9 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 	Relation	newrel;
 	TupleDesc	oldTupDesc;
 	TupleDesc	newTupDesc;
+	TupleDesc 	oldTupDescTemp;
 	bool		needscan = false;
+	bool		scan_only = false;
 	List	   *notnull_attrs;
 	List	   *notnull_virtual_attrs;
 	int			i;
@@ -6135,6 +6143,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 	 */
 	oldrel = table_open(tab->relid, NoLock);
 	oldTupDesc = tab->oldDesc;
+	oldTupDescTemp = CreateTupleDescCopy(oldTupDesc);
 	newTupDesc = RelationGetDescr(oldrel);	/* includes all mods */
 
 	if (OidIsValid(OIDNewHeap))
@@ -6203,6 +6212,11 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 
 		/* expr already planned */
 		ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL);
+		if (ex->scan_only && !tab->rewrite && !scan_only)
+		{
+			needscan = true;
+			scan_only = true;
+		}
 	}
 
 	notnull_attrs = notnull_virtual_attrs = NIL;
@@ -6431,6 +6445,43 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 				 * new constraints etc.
 				 */
 				insertslot = oldslot;
+
+				/*
+				 * The tupdesc (newTupDesc) in oldslot already have the updated
+				 * attribute changes. If we use it for below ExecEvalExpr, then
+				 * CheckVarSlotCompatibility will fail.  Therefore, we need to
+				 * temporarily set oldslot's tts_tupleDescriptor equal to oldTupDesc.
+				 * Essentially, what we're doing here is evaluating the
+				 * CoerceToDomain node against the existing table slot.
+				*/
+				if (scan_only)
+				{
+					Datum		values pg_attribute_unused();
+					bool		isnull pg_attribute_unused();
+
+					insertslot->tts_tupleDescriptor = oldTupDescTemp;
+					econtext->ecxt_scantuple = insertslot;
+
+					foreach(l, tab->newvals)
+					{
+						NewColumnValue *ex = lfirst(l);
+
+						if (!ex->scan_only)
+							continue;
+
+						/*
+						 * we can not use ExecEvalExprNoReturn here, because we
+						 * use ExecInitExpr compile NewColumnValue->expr.  Here,
+						 * we only check whether the oldslot value satisfies the
+						 * domain constraint. So it is ok to override the value
+						 * evaluated by ExecEvalExpr.
+						 */
+						values = ExecEvalExpr(ex->exprstate, econtext, &isnull);
+						values = (Datum) 0;
+						isnull = true;
+					}
+					insertslot->tts_tupleDescriptor = newTupDesc;
+				}
 			}
 
 			/* Now check any constraints on the possibly-changed tuple */
@@ -7466,15 +7517,6 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 		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 */
 		if (!defval && has_domain_constraints)
 		{
@@ -7511,6 +7553,13 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			newval->expr = defval;
 			newval->is_generated = (colDef->generated != '\0');
 
+			/*
+			 * If the domain has volatile constraints, table scan is enough to
+			 * evaluate CoerceToDomain, no need table rewrite.
+			*/
+			if (has_volatile && IsA(defval, CoerceToDomain))
+				newval->scan_only = true;
+
 			tab->newvals = lappend(tab->newvals, newval);
 
 			/*
@@ -7528,7 +7577,6 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			 */
 			if (rel->rd_rel->relkind == RELKIND_RELATION &&
 				!colDef->generated &&
-				!has_volatile &&
 				!contain_volatile_functions((Node *) defval))
 			{
 				EState	   *estate;
@@ -7560,7 +7608,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 				}
 				FreeExecutorState(estate);
 			}
-			else
+			else if (!has_volatile)
 			{
 				/*
 				 * Failed to use missing mode.  We have to do a table rewrite
diff --git a/src/test/regress/expected/fast_default.out b/src/test/regress/expected/fast_default.out
index aa522cf8bfa..136454a289d 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -351,11 +351,34 @@ ORDER BY attnum;
 --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;
+--No need table rewrite when we are applying domain constraint volatile expresion,
+--but table scan is required.
+ALTER TABLE t3 ADD COLUMN f domain8 default 1; --error while coercing to domain
 NOTICE:  rewriting table t3 for reason 2
+ERROR:  value for domain domain8 violates check constraint "domain8_check"
+ALTER TABLE t3 ADD COLUMN f domain8 default 20; --ok
+ALTER DOMAIN domain8 ADD CHECK(VALUE IS NOT NULL);
+--table rewrite then error, because f1 default value (NULL), does not satisfied with domain constraint
 ALTER TABLE t3 ADD COLUMN f1 domain8;
 NOTICE:  rewriting table t3 for reason 2
+ERROR:  value for domain domain8 violates check constraint "domain8_check1"
+SELECT f FROM t3;
+ f  
+----
+ 20
+ 20
+(2 rows)
+
+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 
+--------+---------+---------------+-----------+---------------
+      6 | f       | t             | t         | {20}
+(1 row)
+
 DROP TABLE t2;
 DROP TABLE t3;
 DROP DOMAIN domain1;
diff --git a/src/test/regress/sql/fast_default.sql b/src/test/regress/sql/fast_default.sql
index 269a39edf77..178249de960 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -315,10 +315,21 @@ 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;
+--No need table rewrite when we are applying domain constraint volatile expresion,
+--but table scan is required.
+ALTER TABLE t3 ADD COLUMN f domain8 default 1; --error while coercing to domain
+ALTER TABLE t3 ADD COLUMN f domain8 default 20; --ok
+ALTER DOMAIN domain8 ADD CHECK(VALUE IS NOT NULL);
+--table rewrite then error, because f1 default value (NULL), does not satisfied with domain constraint
 ALTER TABLE t3 ADD COLUMN f1 domain8;
 
+SELECT f FROM t3;
+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;
+
 DROP TABLE t2;
 DROP TABLE t3;
 DROP DOMAIN domain1;
-- 
2.34.1

v7-0002-add-function-DomainHaveVolatileConstraints.patchtext/x-patch; charset=US-ASCII; name=v7-0002-add-function-DomainHaveVolatileConstraints.patchDownload
From da5efb88c112d49a8800e81f857006bb58deee92 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 19 May 2025 11:11:01 +0800
Subject: [PATCH v7 2/4] add function DomainHaveVolatileConstraints

the signature is:
extern bool DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile);

Returns true if the Domain has any constraints.  If you want check this domain
have any volatile check constraints, make sure have_volatile is not NULL.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/utils/cache/typcache.c | 40 ++++++++++++++++++++++++++++++
 src/include/utils/typcache.h       |  1 +
 2 files changed, 41 insertions(+)

diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index 6a347698edf..da9ed0d29c8 100644
--- a/src/backend/utils/cache/typcache.c
+++ b/src/backend/utils/cache/typcache.c
@@ -1499,6 +1499,46 @@ DomainHasConstraints(Oid type_id)
 }
 
 
+/*
+ * Check whether a domain has any constraints, and determine if any of those
+ * constraints contain volatile expressions.
+ *
+ * To detect volatile expressions within domain check constraints, ensure that
+ * have_volatile is not NULL. If have_volatile is NULL, the behavior is
+ * equivalent to that of DomainHasConstraints.
+ */
+bool
+DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile)
+{
+	TypeCacheEntry *typentry;
+
+	/*
+	 * Note: a side effect is to cause the typcache's domain data to become
+	 * valid.  This is fine since we'll likely need it soon if there is any.
+	 */
+	typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO);
+
+	if (typentry->domainData != NULL)
+	{
+		ListCell   *lc;
+
+		foreach(lc, typentry->domainData->constraints)
+		{
+			DomainConstraintState *r = (DomainConstraintState *) lfirst(lc);
+
+			if (r->constrainttype == DOM_CONSTRAINT_CHECK &&
+				contain_volatile_functions((Node *) r->check_expr))
+			{
+				if (have_volatile)
+					*have_volatile = true;
+				break;
+			}
+		}
+		return true;
+	}
+	return false;
+}
+
 /*
  * array_element_has_equality and friends are helper routines to check
  * whether we should believe that array_eq and related functions will work
diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h
index 1cb30f1818c..aa1c86e35c3 100644
--- a/src/include/utils/typcache.h
+++ b/src/include/utils/typcache.h
@@ -184,6 +184,7 @@ extern void InitDomainConstraintRef(Oid type_id, DomainConstraintRef *ref,
 extern void UpdateDomainConstraintRef(DomainConstraintRef *ref);
 
 extern bool DomainHasConstraints(Oid type_id);
+extern bool DomainHaveVolatileConstraints(Oid type_id, bool *have_volatile);
 
 extern TupleDesc lookup_rowtype_tupdesc(Oid type_id, int32 typmod);
 
-- 
2.34.1

v7-0001-soft-error-variant-of-ExecPrepareExpr-ExecInitExpr.patchtext/x-patch; charset=US-ASCII; name=v7-0001-soft-error-variant-of-ExecPrepareExpr-ExecInitExpr.patchDownload
From 12f6bfc936b1d51d7956b035066eacafe287c3f7 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 24 Mar 2025 16:07:46 +0800
Subject: [PATCH v7 1/4] soft error variant of ExecPrepareExpr, ExecInitExpr

ExecInitExprSafe: soft error of ExecInitExpr.
ExecPrepareExprSafe: soft error of ExecPrepareExpr.
ExecPrepareExprSafe initialize for expression execution with soft error support.
not all expression node support it. Like node CoerceToDomain support it.

XXX more comments.

discussion: https://postgr.es/m/CACJufxE_+iZBR1i49k_AHigppPwLTJi6km8NOsC7FWvKdEmmXg@mail.gmail.com
---
 src/backend/executor/execExpr.c | 63 +++++++++++++++++++++++++++++++++
 src/include/executor/executor.h |  2 ++
 2 files changed, 65 insertions(+)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index f1569879b52..9182ba446a0 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -170,6 +170,47 @@ ExecInitExpr(Expr *node, PlanState *parent)
 	return state;
 }
 
+/*
+ * ExecInitExprSafe: soft error variant of ExecInitExpr.
+ *
+ * use it only for expression nodes support soft errors, not all expression
+ * nodes support it.
+*/
+ExprState *
+ExecInitExprSafe(Expr *node, PlanState *parent)
+{
+	ExprState  *state;
+	ExprEvalStep scratch = {0};
+
+	/* Special case: NULL expression produces a NULL ExprState pointer */
+	if (node == NULL)
+		return NULL;
+
+	/* Initialize ExprState with empty step list */
+	state = makeNode(ExprState);
+	state->expr = node;
+	state->parent = parent;
+	state->ext_params = NULL;
+	state->escontext = makeNode(ErrorSaveContext);
+	state->escontext->type = T_ErrorSaveContext;
+	state->escontext->error_occurred = false;
+	state->escontext->details_wanted = true;
+
+	/* Insert setup steps as needed */
+	ExecCreateExprSetupSteps(state, (Node *) node);
+
+	/* Compile the expression proper */
+	ExecInitExprRec(node, state, &state->resvalue, &state->resnull);
+
+	/* Finally, append a DONE step */
+	scratch.opcode = EEOP_DONE_RETURN;
+	ExprEvalPushStep(state, &scratch);
+
+	ExecReadyExpr(state);
+
+	return state;
+}
+
 /*
  * ExecInitExprWithParams: prepare a standalone expression tree for execution
  *
@@ -778,6 +819,28 @@ ExecPrepareExpr(Expr *node, EState *estate)
 	return result;
 }
 
+/*
+ * ExecPrepareExprSafe: soft error variant of ExecPrepareExpr.
+ *
+ * use it when expression node *support* soft error expression execution.
+ */
+ExprState *
+ExecPrepareExprSafe(Expr *node, EState *estate)
+{
+	ExprState  *result;
+	MemoryContext oldcontext;
+
+	oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
+
+	node = expression_planner(node);
+
+	result = ExecInitExprSafe(node, NULL);
+
+	MemoryContextSwitchTo(oldcontext);
+
+	return result;
+}
+
 /*
  * ExecPrepareQual --- initialize for qual execution outside a normal
  * Plan tree context.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 10dcea037c3..eb5ecf9d59f 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -320,6 +320,7 @@ ExecProcNode(PlanState *node)
  * prototypes from functions in execExpr.c
  */
 extern ExprState *ExecInitExpr(Expr *node, PlanState *parent);
+extern ExprState *ExecInitExprSafe(Expr *node, PlanState *parent);
 extern ExprState *ExecInitExprWithParams(Expr *node, ParamListInfo ext_params);
 extern ExprState *ExecInitQual(List *qual, PlanState *parent);
 extern ExprState *ExecInitCheck(List *qual, PlanState *parent);
@@ -368,6 +369,7 @@ extern ProjectionInfo *ExecBuildUpdateProjection(List *targetList,
 												 TupleTableSlot *slot,
 												 PlanState *parent);
 extern ExprState *ExecPrepareExpr(Expr *node, EState *estate);
+extern ExprState *ExecPrepareExprSafe(Expr *node, EState *estate);
 extern ExprState *ExecPrepareQual(List *qual, EState *estate);
 extern ExprState *ExecPrepareCheck(List *qual, EState *estate);
 extern List *ExecPrepareExprList(List *nodes, EState *estate);
-- 
2.34.1