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/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/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/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); 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); 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