On 2026-03-11 We 6:34 AM, Viktor Holmberg wrote:
I’ve been burned my this issue in the past so would be great if this
could get in.
+/*
+ * DomainHasVolatileConstraints --- check if a domain has constraints
with
+ * volatile expressions
+ *
+ * Returns true if the domain has any constraints at all. If
have_volatile
+ * is not NULL, also checks whether any CHECK constraint contains a
volatile
+ * expression and sets *have_volatile accordingly.
+ *
+ * The caller must initialize *have_volatile before calling (typically to
+ * false). This function only ever sets it to true, never to false.
+ *
+ * This is defined to return false, not fail, if type is not a domain.
+ */
+bool
+DomainHasVolatileConstraints(Oid type_id, bool *have_volatile)
Call it CheckDomainConstraints or something instead? IMO it's confusing
the have it not return what it's called.
Also, it'd make it more self-contained and thus safer to initialise
have_volatile to false.
+ if (typentry->domainData != NULL)
+ {
+ if (have_volatile)
+ {
+ foreach_node(DomainConstraintState, constrstate,
+ typentry->domainData->constraints)
+ {
+ if (constrstate->constrainttype == DOM_CONSTRAINT_CHECK &&
+ contain_volatile_functions((Node *) constrstate->check_expr))
+ {
+ *have_volatile = true;
+ break;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+}
Could simplify the code by doing an early return if domainData == NULL?
(same with have_volatile below)
I think it's cleaner just to modify the existing function with an extra
parameter, which the existing callers will pass as NULL.
Would be nice to test domains with both volatile and non-volatile checks.
Also, perhaps virtual generated columns could use a test?
Also added some tests.
cheers
andrew
--
Andrew Dunstan
EDB:https://www.enterprisedb.com
From 44d46983ab7a7277a75bd684df7f5e68fc18b672 Mon Sep 17 00:00:00 2001
From: Jian He <[email protected]>
Date: Wed, 11 Mar 2026 15:42:35 -0400
Subject: [PATCH v11 1/2] Extend DomainHasConstraints() to optionally check
constraint volatility
Add an optional bool *have_volatile output parameter to
DomainHasConstraints(). When non-NULL, the function checks whether any
CHECK constraint contains a volatile expression. Callers that don't
need this information pass NULL and get the same behavior as before.
This is needed by a subsequent commit that enables the fast default
optimization for domains with non-volatile constraints: we can safely
evaluate such constraints once at ALTER TABLE time, but volatile
constraints require a full table rewrite.
Author: Jian He <[email protected]>
Reviewed-by: Tom Lane <[email protected]>
Reviewed-by: Andrew Dunstan <[email protected]>
Reviewed-by: Viktor Holmberg <[email protected]>
Discussion: https://postgr.es/m/cacjufxe_+izbr1i49k_ahigpppwltji6km8nosc7fwvkdem...@mail.gmail.com
---
src/backend/commands/copyfrom.c | 2 +-
src/backend/commands/tablecmds.c | 4 ++--
src/backend/executor/execExpr.c | 2 +-
src/backend/optimizer/util/clauses.c | 2 +-
src/backend/parser/parse_expr.c | 2 +-
src/backend/utils/cache/typcache.c | 27 +++++++++++++++++++++++++--
src/include/utils/typcache.h | 2 +-
7 files changed, 32 insertions(+), 9 deletions(-)
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 2f42f55e229..0ece40557c8 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -1653,7 +1653,7 @@ BeginCopyFrom(ParseState *pstate,
Form_pg_attribute att = TupleDescAttr(tupDesc, attno - 1);
- cstate->domain_with_constraint[i] = DomainHasConstraints(att->atttypid);
+ cstate->domain_with_constraint[i] = DomainHasConstraints(att->atttypid, NULL);
}
}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 85242dcc245..72fec5937d9 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7523,7 +7523,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
defval = (Expr *) build_column_default(rel, attribute->attnum);
/* Build CoerceToDomain(NULL) expression if needed */
- has_domain_constraints = DomainHasConstraints(attribute->atttypid);
+ has_domain_constraints = DomainHasConstraints(attribute->atttypid, NULL);
if (!defval && has_domain_constraints)
{
Oid baseTypeId;
@@ -14718,7 +14718,7 @@ ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno)
{
CoerceToDomain *d = (CoerceToDomain *) expr;
- if (DomainHasConstraints(d->resulttype))
+ if (DomainHasConstraints(d->resulttype, NULL))
return true;
expr = (Node *) d->arg;
}
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 088eca24021..b96eb0c55cc 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -5061,6 +5061,6 @@ ExecInitJsonCoercion(ExprState *state, JsonReturning *returning,
scratch.d.jsonexpr_coercion.exists_cast_to_int = exists_coerce &&
getBaseType(returning->typid) == INT4OID;
scratch.d.jsonexpr_coercion.exists_check_domain = exists_coerce &&
- DomainHasConstraints(returning->typid);
+ DomainHasConstraints(returning->typid, NULL);
ExprEvalPushStep(state, &scratch);
}
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index a41d81734cf..06a4bd59d47 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -3803,7 +3803,7 @@ eval_const_expressions_mutator(Node *node,
arg = eval_const_expressions_mutator((Node *) cdomain->arg,
context);
if (context->estimate ||
- !DomainHasConstraints(cdomain->resulttype))
+ !DomainHasConstraints(cdomain->resulttype, NULL))
{
/* Record dependency, if this isn't estimation mode */
if (context->root && !context->estimate)
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index dcfe1acc4c3..96991cae764 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -4628,7 +4628,7 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
if (jsexpr->returning->typid != TEXTOID)
{
if (get_typtype(jsexpr->returning->typid) == TYPTYPE_DOMAIN &&
- DomainHasConstraints(jsexpr->returning->typid))
+ DomainHasConstraints(jsexpr->returning->typid, NULL))
jsexpr->use_json_coercion = true;
else
jsexpr->use_io_coercion = true;
diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index 627e534609a..20a8eeb6393 100644
--- a/src/backend/utils/cache/typcache.c
+++ b/src/backend/utils/cache/typcache.c
@@ -1485,10 +1485,14 @@ UpdateDomainConstraintRef(DomainConstraintRef *ref)
/*
* DomainHasConstraints --- utility routine to check if a domain has constraints
*
+ * Returns true if the domain has any constraints at all. If have_volatile
+ * is not NULL, also checks whether any CHECK constraint contains a volatile
+ * expression and sets *have_volatile accordingly.
+ *
* This is defined to return false, not fail, if type is not a domain.
*/
bool
-DomainHasConstraints(Oid type_id)
+DomainHasConstraints(Oid type_id, bool *have_volatile)
{
TypeCacheEntry *typentry;
@@ -1498,7 +1502,26 @@ DomainHasConstraints(Oid type_id)
*/
typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO);
- return (typentry->domainData != NULL);
+ if (typentry->domainData == NULL)
+ return false;
+
+ if (have_volatile)
+ {
+ *have_volatile = false;
+
+ foreach_node(DomainConstraintState, constrstate,
+ typentry->domainData->constraints)
+ {
+ if (constrstate->constrainttype == DOM_CONSTRAINT_CHECK &&
+ contain_volatile_functions((Node *) constrstate->check_expr))
+ {
+ *have_volatile = true;
+ break;
+ }
+ }
+ }
+
+ return true;
}
diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h
index 0e3945aa244..0707b79bbec 100644
--- a/src/include/utils/typcache.h
+++ b/src/include/utils/typcache.h
@@ -183,7 +183,7 @@ extern void InitDomainConstraintRef(Oid type_id, DomainConstraintRef *ref,
extern void UpdateDomainConstraintRef(DomainConstraintRef *ref);
-extern bool DomainHasConstraints(Oid type_id);
+extern bool DomainHasConstraints(Oid type_id, bool *have_volatile);
extern TupleDesc lookup_rowtype_tupdesc(Oid type_id, int32 typmod);
--
2.43.0
From abb06614aa663b427a93f4e3a22fea49e0975c7c Mon Sep 17 00:00:00 2001
From: Jian He <[email protected]>
Date: Wed, 11 Mar 2026 15:44:44 -0400
Subject: [PATCH v11 2/2] Enable fast default for domains with non-volatile
constraints
Previously, ALTER TABLE ADD COLUMN always forced a table rewrite when
the column type was a domain with constraints (CHECK or NOT NULL), even
if the default value satisfied those constraints. This was because
contain_volatile_functions() considers CoerceToDomain immutable, so
the code conservatively assumed any constrained domain might fail.
Improve this by using soft error handling (ErrorSaveContext) to evaluate
the CoerceToDomain expression at ALTER TABLE time. If the default value
passes the domain's constraints, the value is stored as a "missing"
attribute default and no table rewrite is needed. If the constraint
check fails, we fall back to a table rewrite, preserving the historical
behavior that constraint violations are only raised when the table
actually contains rows.
Domains with volatile constraint expressions always require a table
rewrite since the constraint result could differ per evaluation and
cannot be cached.
Author: Jian He <[email protected]>
Reviewed-by: Tom Lane <[email protected]>
Reviewed-by: Andrew Dunstan <[email protected]>
Reviewed-by: Viktor Holmberg <[email protected]>
Discussion: https://postgr.es/m/cacjufxe_+izbr1i49k_ahigpppwltji6km8nosc7fwvkdem...@mail.gmail.com
---
src/backend/commands/tablecmds.c | 60 +++++++++++++++------
src/backend/executor/execExpr.c | 35 ++++++++++++-
src/include/executor/executor.h | 2 +
src/test/regress/expected/fast_default.out | 61 ++++++++++++++++++++++
src/test/regress/sql/fast_default.sql | 51 ++++++++++++++++++
5 files changed, 191 insertions(+), 18 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 72fec5937d9..10fd8b008c4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7478,15 +7478,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.
@@ -7505,6 +7496,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(),
@@ -7522,8 +7514,20 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
else
defval = (Expr *) build_column_default(rel, attribute->attnum);
+ has_domain_constraints =
+ DomainHasConstraints(attribute->atttypid, &has_volatile);
+
+ /*
+ * If the domain has volatile constraints, we must do a table rewrite
+ * since the constraint result could differ per row and cannot be
+ * evaluated once and cached as a missing value.
+ */
+ if (has_volatile)
+ {
+ tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+ }
+
/* Build CoerceToDomain(NULL) expression if needed */
- has_domain_constraints = DomainHasConstraints(attribute->atttypid, NULL);
if (!defval && has_domain_constraints)
{
Oid baseTypeId;
@@ -7565,27 +7569,49 @@ 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 expressions can't be volatile (stable is OK).
+ *
+ * Note that contain_volatile_functions considers CoerceToDomain
+ * immutable, so we rely on DomainHasConstraints (called above)
+ * rather than checking defval alone.
+ *
+ * For domains with non-volatile constraints, we evaluate the
+ * default using soft error handling: if the constraint check
+ * fails (e.g., CHECK(value > 10) with DEFAULT 8), we fall back to
+ * a table rewrite. This preserves the historical behavior that
+ * such a failure is only raised when the table has rows.
*/
if (rel->rd_rel->relkind == RELKIND_RELATION &&
!colDef->generated &&
- !has_domain_constraints &&
+ !has_volatile &&
!contain_volatile_functions((Node *) defval))
{
EState *estate;
ExprState *exprState;
Datum missingval;
bool missingIsNull;
+ ErrorSaveContext escontext = {T_ErrorSaveContext};
- /* Evaluate the default expression */
+ /* Evaluate the default expression with soft errors */
estate = CreateExecutorState();
- exprState = ExecPrepareExpr(defval, estate);
+ exprState = ExecPrepareExprWithContext(defval, estate,
+ (Node *) &escontext);
missingval = ExecEvalExpr(exprState,
GetPerTupleExprContext(estate),
&missingIsNull);
+
+ /*
+ * If the domain constraint check failed, fall back to a table
+ * rewrite. Phase 3 will re-evaluate with hard errors, so the
+ * user gets an error only if the table has rows.
+ */
+ if (SOFT_ERROR_OCCURRED(&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/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index b96eb0c55cc..bd46b75e498 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -141,6 +141,26 @@ static void ExecInitJsonCoercion(ExprState *state, JsonReturning *returning,
*/
ExprState *
ExecInitExpr(Expr *node, PlanState *parent)
+{
+ return ExecInitExprWithContext(node, parent, NULL);
+}
+
+/*
+ * ExecInitExprWithContext: same as ExecInitExpr, but with an optional
+ * ErrorSaveContext for soft error handling.
+ *
+ * When 'escontext' is non-NULL, expression nodes that support soft errors
+ * (currently CoerceToDomain's NOT NULL and CHECK constraint steps) will use
+ * errsave() instead of ereport(), allowing the caller to detect and handle
+ * failures without a transaction abort.
+ *
+ * The escontext must be provided at initialization time (not after), because
+ * it is copied into per-step data during expression compilation.
+ *
+ * Not all expression node types support soft errors. If in doubt, pass NULL.
+ */
+ExprState *
+ExecInitExprWithContext(Expr *node, PlanState *parent, Node *escontext)
{
ExprState *state;
ExprEvalStep scratch = {0};
@@ -154,6 +174,7 @@ ExecInitExpr(Expr *node, PlanState *parent)
state->expr = node;
state->parent = parent;
state->ext_params = NULL;
+ state->escontext = (ErrorSaveContext *) escontext;
/* Insert setup steps as needed */
ExecCreateExprSetupSteps(state, (Node *) node);
@@ -763,6 +784,18 @@ ExecBuildUpdateProjection(List *targetList,
*/
ExprState *
ExecPrepareExpr(Expr *node, EState *estate)
+{
+ return ExecPrepareExprWithContext(node, estate, NULL);
+}
+
+/*
+ * ExecPrepareExprWithContext: same as ExecPrepareExpr, but with an optional
+ * ErrorSaveContext for soft error handling.
+ *
+ * See ExecInitExprWithContext for details on the escontext parameter.
+ */
+ExprState *
+ExecPrepareExprWithContext(Expr *node, EState *estate, Node *escontext)
{
ExprState *result;
MemoryContext oldcontext;
@@ -771,7 +804,7 @@ ExecPrepareExpr(Expr *node, EState *estate)
node = expression_planner(node);
- result = ExecInitExpr(node, NULL);
+ result = ExecInitExprWithContext(node, NULL, escontext);
MemoryContextSwitchTo(oldcontext);
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index d46ba59895d..82c442d23f8 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -324,6 +324,7 @@ ExecProcNode(PlanState *node)
* prototypes from functions in execExpr.c
*/
extern ExprState *ExecInitExpr(Expr *node, PlanState *parent);
+extern ExprState *ExecInitExprWithContext(Expr *node, PlanState *parent, Node *escontext);
extern ExprState *ExecInitExprWithParams(Expr *node, ParamListInfo ext_params);
extern ExprState *ExecInitQual(List *qual, PlanState *parent);
extern ExprState *ExecInitCheck(List *qual, PlanState *parent);
@@ -372,6 +373,7 @@ extern ProjectionInfo *ExecBuildUpdateProjection(List *targetList,
TupleTableSlot *slot,
PlanState *parent);
extern ExprState *ExecPrepareExpr(Expr *node, EState *estate);
+extern ExprState *ExecPrepareExprWithContext(Expr *node, EState *estate, Node *escontext);
extern ExprState *ExecPrepareQual(List *qual, EState *estate);
extern ExprState *ExecPrepareCheck(List *qual, EState *estate);
extern List *ExecPrepareExprList(List *nodes, EState *estate);
diff --git a/src/test/regress/expected/fast_default.out b/src/test/regress/expected/fast_default.out
index ccbcdf8403f..31a7e480ab6 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -317,11 +317,72 @@ 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 CHECK(value > 10) DEFAULT random(min=>11, max=>100);
+CREATE DOMAIN domain7 as int CHECK((value + random(min=>11::int, max=>11)) > 12);
+CREATE DOMAIN domain8 as int NOT NULL;
+CREATE TABLE test_add_domain_col(a int);
+-- table rewrite, not fail because test_add_domain_col is empty table
+ALTER TABLE test_add_domain_col ADD COLUMN a1 domain5;
+NOTICE: rewriting table test_add_domain_col for reason 2
+ALTER TABLE test_add_domain_col DROP COLUMN a1;
+INSERT INTO test_add_domain_col VALUES(1),(2);
+-- tests with non-empty table
+ALTER TABLE test_add_domain_col ADD COLUMN b domain5; -- table rewrite, then fail
+NOTICE: rewriting table test_add_domain_col for reason 2
+ERROR: value for domain domain5 violates check constraint "domain5_check"
+ALTER TABLE test_add_domain_col ADD COLUMN b domain8; -- table rewrite, then fail
+NOTICE: rewriting table test_add_domain_col for reason 2
+ERROR: domain domain8 does not allow null values
+ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 1; -- table rewrite, then fail
+NOTICE: rewriting table test_add_domain_col for reason 2
+ERROR: value for domain domain5 violates check constraint "domain5_check"
+ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 12; -- ok, no table rewrite
+-- explicit column default expression overrides domain's default
+-- expression, so no table rewrite
+ALTER TABLE test_add_domain_col ADD COLUMN c domain6 DEFAULT 14;
+ALTER TABLE test_add_domain_col ADD COLUMN c1 domain8 DEFAULT 13; -- no table rewrite
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 'test_add_domain_col'::regclass AND attisdropped is false
+AND atthasmissing
+ORDER BY attnum;
+ attnum | attname | atthasmissing | atthasdef | attmissingval
+--------+---------+---------------+-----------+---------------
+ 3 | b | t | t | {12}
+ 4 | c | t | t | {14}
+ 5 | c1 | t | t | {13}
+(3 rows)
+
+-- We need to rewrite the table whenever domain default contains volatile expression
+ALTER TABLE test_add_domain_col ADD COLUMN d domain6;
+NOTICE: rewriting table test_add_domain_col for reason 2
+-- We need to rewrite the table whenever domain constraint expression contains volatile expression
+ALTER TABLE test_add_domain_col ADD COLUMN e domain7 default 14;
+NOTICE: rewriting table test_add_domain_col for reason 2
+ALTER TABLE test_add_domain_col ADD COLUMN f domain7;
+NOTICE: rewriting table test_add_domain_col for reason 2
+-- domain with both volatile and non-volatile CHECK constraints: the
+-- volatile one forces a table rewrite
+CREATE DOMAIN domain9 AS int CHECK(value > 10) CHECK((value + random(min=>1::int, max=>1)) > 0);
+ALTER TABLE test_add_domain_col ADD COLUMN g domain9 DEFAULT 14;
+NOTICE: rewriting table test_add_domain_col for reason 2
+-- virtual generated columns cannot have domain types
+ALTER TABLE test_add_domain_col ADD COLUMN h domain5
+ GENERATED ALWAYS AS (a + 20) VIRTUAL; -- error
+ERROR: virtual generated column "h" cannot have a domain type
DROP TABLE t2;
+DROP TABLE test_add_domain_col;
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..f92f8250512 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -287,11 +287,62 @@ 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 CHECK(value > 10) DEFAULT random(min=>11, max=>100);
+CREATE DOMAIN domain7 as int CHECK((value + random(min=>11::int, max=>11)) > 12);
+CREATE DOMAIN domain8 as int NOT NULL;
+
+CREATE TABLE test_add_domain_col(a int);
+-- table rewrite, not fail because test_add_domain_col is empty table
+ALTER TABLE test_add_domain_col ADD COLUMN a1 domain5;
+ALTER TABLE test_add_domain_col DROP COLUMN a1;
+INSERT INTO test_add_domain_col VALUES(1),(2);
+
+-- tests with non-empty table
+ALTER TABLE test_add_domain_col ADD COLUMN b domain5; -- table rewrite, then fail
+ALTER TABLE test_add_domain_col ADD COLUMN b domain8; -- table rewrite, then fail
+ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 1; -- table rewrite, then fail
+ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 12; -- ok, no table rewrite
+
+-- explicit column default expression overrides domain's default
+-- expression, so no table rewrite
+ALTER TABLE test_add_domain_col ADD COLUMN c domain6 DEFAULT 14;
+
+ALTER TABLE test_add_domain_col ADD COLUMN c1 domain8 DEFAULT 13; -- no table rewrite
+SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
+FROM pg_attribute
+WHERE attnum > 0 AND attrelid = 'test_add_domain_col'::regclass AND attisdropped is false
+AND atthasmissing
+ORDER BY attnum;
+
+-- We need to rewrite the table whenever domain default contains volatile expression
+ALTER TABLE test_add_domain_col ADD COLUMN d domain6;
+
+-- We need to rewrite the table whenever domain constraint expression contains volatile expression
+ALTER TABLE test_add_domain_col ADD COLUMN e domain7 default 14;
+ALTER TABLE test_add_domain_col ADD COLUMN f domain7;
+
+-- domain with both volatile and non-volatile CHECK constraints: the
+-- volatile one forces a table rewrite
+CREATE DOMAIN domain9 AS int CHECK(value > 10) CHECK((value + random(min=>1::int, max=>1)) > 0);
+ALTER TABLE test_add_domain_col ADD COLUMN g domain9 DEFAULT 14;
+
+-- virtual generated columns cannot have domain types
+ALTER TABLE test_add_domain_col ADD COLUMN h domain5
+ GENERATED ALWAYS AS (a + 20) VIRTUAL; -- error
+
DROP TABLE t2;
+DROP TABLE test_add_domain_col;
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.43.0