diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2705cf11330d..2467d4eef41e 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,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(), @@ -7473,8 +7466,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; @@ -7495,6 +7511,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) @@ -7517,13 +7540,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; @@ -7533,10 +7559,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/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b529..9182ba446a0b 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 e359da09ec9f..5d4e126d68ee 100644 --- a/src/backend/utils/cache/typcache.c +++ b/src/backend/utils/cache/typcache.c @@ -1500,6 +1500,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/executor/executor.h b/src/include/executor/executor.h index ae99407db89d..a26160042eed 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); diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h index 1cb30f1818c2..aa1c86e35c3b 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 ccbcdf8403fa..73fb23fd79bc 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 068dd0bc8aad..e9187cd2198e 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