aboutsummaryrefslogtreecommitdiff
path: root/src/backend/commands/tablecmds.c
diff options
context:
space:
mode:
authorTom Lane <tgl@sss.pgh.pa.us>2025-03-03 12:43:29 -0500
committerTom Lane <tgl@sss.pgh.pa.us>2025-03-03 12:43:44 -0500
commit95f650674d2ceea1ba6440a9b0ae89ed3867fd7e (patch)
treea26041583a5dd5e48286bf58d99fa103a262ebc9 /src/backend/commands/tablecmds.c
parent99f8f3fbbc8f743290844e8c676d39dad11c5d5d (diff)
downloadpostgresql-95f650674d2ceea1ba6440a9b0ae89ed3867fd7e.tar.gz
postgresql-95f650674d2ceea1ba6440a9b0ae89ed3867fd7e.zip
Fix broken handling of domains in atthasmissing logic.
If a domain type has a default, adding a column of that type (without any explicit DEFAULT clause) failed to install the domain's default value in existing rows, instead leaving the new column null. This is unexpected, and it used to work correctly before v11. The cause is confusion in the atthasmissing mechanism about which default value to install: we'd only consider installing an explicitly-specified default, and then we'd decide that no table rewrite is needed. To fix, take the responsibility for filling attmissingval out of StoreAttrDefault, and instead put it into ATExecAddColumn's existing logic that derives the correct value to fill the new column with. Also, centralize the logic that determines the need for default-related table rewriting there, instead of spreading it over four or five places. In the back branches, we'll leave the attmissingval-filling code in StoreAttrDefault even though it's now dead, for fear that some extension may be depending on that functionality to exist there. A separate HEAD-only patch will clean up the now-useless code. Reported-by: jian he <jian.universality@gmail.com> Author: jian he <jian.universality@gmail.com> Author: Tom Lane <tgl@sss.pgh.pa.us> Discussion: https://postgr.es/m/CACJufxHFssPvkP1we7WMhPD_1kwgbG52o=kQgL+TnVoX5LOyCQ@mail.gmail.com Backpatch-through: 13
Diffstat (limited to 'src/backend/commands/tablecmds.c')
-rw-r--r--src/backend/commands/tablecmds.c87
1 files changed, 60 insertions, 27 deletions
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ce7d115667e..fd31216b27b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7321,14 +7321,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault));
rawEnt->attnum = attribute->attnum;
rawEnt->raw_default = copyObject(colDef->raw_default);
-
- /*
- * Attempt to skip a complete table rewrite by storing the specified
- * DEFAULT value outside of the heap. This may be disabled inside
- * AddRelationNewConstraints if the optimization cannot be applied.
- */
- rawEnt->missingMode = (colDef->generated != ATTRIBUTE_GENERATED_STORED);
-
+ rawEnt->missingMode = false; /* XXX vestigial */
rawEnt->generated = colDef->generated;
/*
@@ -7340,13 +7333,6 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
/* Make the additional catalog changes visible */
CommandCounterIncrement();
-
- /*
- * Did the request for a missing value work? If not we'll have to do a
- * rewrite
- */
- if (!rawEnt->missingMode)
- tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
}
/*
@@ -7363,9 +7349,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
* 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 don't have to do, but the present
- * design of domain processing doesn't offer any simple way of checking
- * the constraints more directly.)
+ * 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
@@ -7384,6 +7370,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
*/
if (RELKIND_HAS_STORAGE(relkind))
{
+ bool has_domain_constraints;
+ bool has_missing = false;
+
/*
* For an identity column, we can't use build_column_default(),
* because the sequence ownership isn't set yet. So do it manually.
@@ -7396,14 +7385,13 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
nve->typeId = attribute->atttypid;
defval = (Expr *) nve;
-
- /* must do a rewrite for identity columns */
- tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
}
else
defval = (Expr *) build_column_default(rel, attribute->attnum);
- if (!defval && DomainHasConstraints(attribute->atttypid))
+ /* Build CoerceToDomain(NULL) expression if needed */
+ has_domain_constraints = DomainHasConstraints(attribute->atttypid);
+ if (!defval && has_domain_constraints)
{
Oid baseTypeId;
int32 baseTypeMod;
@@ -7429,18 +7417,63 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
{
NewColumnValue *newval;
+ /* Prepare defval for execution, either here or in Phase 3 */
+ defval = expression_planner(defval);
+
+ /* Add the new default to the newvals list */
newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue));
newval->attnum = attribute->attnum;
- newval->expr = expression_planner(defval);
+ newval->expr = defval;
newval->is_generated = (colDef->generated != '\0');
tab->newvals = lappend(tab->newvals, newval);
- }
- if (DomainHasConstraints(attribute->atttypid))
- tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+ /*
+ * 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.
+ */
+ if (rel->rd_rel->relkind == RELKIND_RELATION &&
+ !colDef->generated &&
+ !has_domain_constraints &&
+ !contain_volatile_functions((Node *) defval))
+ {
+ EState *estate;
+ ExprState *exprState;
+ Datum missingval;
+ bool missingIsNull;
+
+ /* Evaluate the default expression */
+ estate = CreateExecutorState();
+ exprState = ExecPrepareExpr(defval, estate);
+ missingval = ExecEvalExpr(exprState,
+ GetPerTupleExprContext(estate),
+ &missingIsNull);
+ /* If it turns out NULL, nothing to do; else store it */
+ if (!missingIsNull)
+ {
+ StoreAttrMissingVal(rel, attribute->attnum, missingval);
+ has_missing = true;
+ }
+ FreeExecutorState(estate);
+ }
+ else
+ {
+ /*
+ * Failed to use missing mode. We have to do a table rewrite
+ * to install the value --- unless it's a virtual generated
+ * column.
+ */
+ if (colDef->generated != ATTRIBUTE_GENERATED_VIRTUAL)
+ tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+ }
+ }
- if (!TupleDescAttr(rel->rd_att, attribute->attnum - 1)->atthasmissing)
+ if (!has_missing)
{
/*
* If the new column is NOT NULL, and there is no missing value,