]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Revert "Enable fast default for domains with non-volatile constraints"
authorAndrew Dunstan <andrew@dunslane.net>
Mon, 8 Jun 2026 17:42:50 +0000 (13:42 -0400)
committerAndrew Dunstan <andrew@dunslane.net>
Mon, 8 Jun 2026 18:20:39 +0000 (14:20 -0400)
This reverts commit a0b6ef29a51818a4073a5f390ed10ef6453d5c11, along with
its follow-up 2e123e3c2bd34f2377212a4e7cfcdbf9e2d9c7ff ("Silence compiler
warning from older compilers"), which only adjusted code introduced by
the former.

The change failed with an empty table and an invalid default, and the
best way to deal with that will involve an addition to the TAM API, so
it's not ready for relese 19 now.

Discussion: https://postgr.es/m/7033D663-DDB4-4B35-922C-F33DE53B1502@gmail.com

src/backend/commands/tablecmds.c
src/backend/executor/execExpr.c
src/include/executor/executor.h
src/test/regress/expected/fast_default.out
src/test/regress/sql/fast_default.sql

index a1845240a98c457389024e74c00a8186e06be61f..a33e22e8e61847c1dcad6bd069282d782adbfd6d 100644 (file)
@@ -7529,6 +7529,15 @@ 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.
@@ -7547,7 +7556,6 @@ 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(),
@@ -7565,18 +7573,8 @@ 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;
@@ -7618,50 +7616,27 @@ 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), 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.
+                        * 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.
                         */
                        if (rel->rd_rel->relkind == RELKIND_RELATION &&
                                !colDef->generated &&
-                               !has_volatile &&
+                               !has_domain_constraints &&
                                !contain_volatile_functions((Node *) defval))
                        {
                                EState     *estate;
                                ExprState  *exprState;
                                Datum           missingval;
                                bool            missingIsNull;
-                               ErrorSaveContext escontext = {T_ErrorSaveContext};
 
-                               /* Evaluate the default expression with soft errors */
+                               /* Evaluate the default expression */
                                estate = CreateExecutorState();
-                               exprState = ExecPrepareExprWithContext(defval, estate,
-                                                                                                          (Node *) &escontext);
+                               exprState = ExecPrepareExpr(defval, estate);
                                missingval = ExecEvalExpr(exprState,
                                                                                  GetPerTupleExprContext(estate),
                                                                                  &missingIsNull);
-
-                               /*
-                                * If the domain constraint check failed (via errsave),
-                                * missingval is unreliable.  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 (escontext.error_occurred)
-                               {
-                                       missingIsNull = true;
-                                       tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
-                               }
-
                                /* If it turns out NULL, nothing to do; else store it */
                                if (!missingIsNull)
                                {
index 77229141b380df3a9fe8950534eef74af82ce1c8..cfea7e160c24a43aa50ee35f7fed9a6e5c9d71bc 100644 (file)
@@ -141,26 +141,6 @@ 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};
@@ -174,7 +154,6 @@ ExecInitExprWithContext(Expr *node, PlanState *parent, Node *escontext)
        state->expr = node;
        state->parent = parent;
        state->ext_params = NULL;
-       state->escontext = (ErrorSaveContext *) escontext;
 
        /* Insert setup steps as needed */
        ExecCreateExprSetupSteps(state, (Node *) node);
@@ -784,18 +763,6 @@ 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;
@@ -804,7 +771,7 @@ ExecPrepareExprWithContext(Expr *node, EState *estate, Node *escontext)
 
        node = expression_planner(node);
 
-       result = ExecInitExprWithContext(node, NULL, escontext);
+       result = ExecInitExpr(node, NULL);
 
        MemoryContextSwitchTo(oldcontext);
 
index 33bbdbfeffb50af41ec0c935f99718e5d407a9de..650baab3efcaad26e579ac1becd73f0bf20b5860 100644 (file)
@@ -332,7 +332,6 @@ 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);
@@ -381,7 +380,6 @@ 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);
index 5813f1c61a542306a7197aa621707434b44e3c6c..49485f5ec6a753bd911fecf43044def455c0de86 100644 (file)
@@ -317,72 +317,11 @@ 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);
--- succeeds despite constraint-violating default because table is empty
-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);
index e5d9a3d2fd18d32c1cfa3fc4b68d283b4d41ec80..ccd138c4efc40561da1724c14ead3a1b8d9c3dfe 100644 (file)
@@ -287,62 +287,11 @@ 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);
--- succeeds despite constraint-violating default because table is empty
-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