1
0
mirror of https://github.com/postgres/postgres.git synced 2025-06-07 11:02:12 +03:00

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
This commit is contained in:
Tom Lane 2025-03-03 12:43:29 -05:00
parent 5c64ece8aa
commit edc3bccd0d
6 changed files with 233 additions and 34 deletions

View File

@ -68,6 +68,7 @@
#include "pgstat.h" #include "pgstat.h"
#include "storage/lmgr.h" #include "storage/lmgr.h"
#include "storage/predicate.h" #include "storage/predicate.h"
#include "utils/array.h"
#include "utils/builtins.h" #include "utils/builtins.h"
#include "utils/fmgroids.h" #include "utils/fmgroids.h"
#include "utils/inval.h" #include "utils/inval.h"
@ -2002,6 +2003,60 @@ RelationClearMissing(Relation rel)
table_close(attr_rel, RowExclusiveLock); table_close(attr_rel, RowExclusiveLock);
} }
/*
* StoreAttrMissingVal
*
* Set the missing value of a single attribute.
*/
void
StoreAttrMissingVal(Relation rel, AttrNumber attnum, Datum missingval)
{
Datum valuesAtt[Natts_pg_attribute] = {0};
bool nullsAtt[Natts_pg_attribute] = {0};
bool replacesAtt[Natts_pg_attribute] = {0};
Relation attrrel;
Form_pg_attribute attStruct;
HeapTuple atttup,
newtup;
/* This is only supported for plain tables */
Assert(rel->rd_rel->relkind == RELKIND_RELATION);
/* Fetch the pg_attribute row */
attrrel = table_open(AttributeRelationId, RowExclusiveLock);
atttup = SearchSysCache2(ATTNUM,
ObjectIdGetDatum(RelationGetRelid(rel)),
Int16GetDatum(attnum));
if (!HeapTupleIsValid(atttup)) /* shouldn't happen */
elog(ERROR, "cache lookup failed for attribute %d of relation %u",
attnum, RelationGetRelid(rel));
attStruct = (Form_pg_attribute) GETSTRUCT(atttup);
/* Make a one-element array containing the value */
missingval = PointerGetDatum(construct_array(&missingval,
1,
attStruct->atttypid,
attStruct->attlen,
attStruct->attbyval,
attStruct->attalign));
/* Update the pg_attribute row */
valuesAtt[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(true);
replacesAtt[Anum_pg_attribute_atthasmissing - 1] = true;
valuesAtt[Anum_pg_attribute_attmissingval - 1] = missingval;
replacesAtt[Anum_pg_attribute_attmissingval - 1] = true;
newtup = heap_modify_tuple(atttup, RelationGetDescr(attrrel),
valuesAtt, nullsAtt, replacesAtt);
CatalogTupleUpdate(attrrel, &newtup->t_self, newtup);
/* clean up */
ReleaseSysCache(atttup);
table_close(attrrel, RowExclusiveLock);
}
/* /*
* SetAttrMissing * SetAttrMissing
* *
@ -2329,13 +2384,8 @@ AddRelationNewConstraints(Relation rel,
castNode(Const, expr)->constisnull)) castNode(Const, expr)->constisnull))
continue; continue;
/* If the DEFAULT is volatile we cannot use a missing value */
if (colDef->missingMode &&
contain_volatile_functions_after_planning((Expr *) expr))
colDef->missingMode = false;
defOid = StoreAttrDefault(rel, colDef->attnum, expr, is_internal, defOid = StoreAttrDefault(rel, colDef->attnum, expr, is_internal,
colDef->missingMode); false);
cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint)); cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint));
cooked->contype = CONSTR_DEFAULT; cooked->contype = CONSTR_DEFAULT;

View File

@ -120,6 +120,12 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
valuesAtt[Anum_pg_attribute_atthasdef - 1] = true; valuesAtt[Anum_pg_attribute_atthasdef - 1] = true;
replacesAtt[Anum_pg_attribute_atthasdef - 1] = true; replacesAtt[Anum_pg_attribute_atthasdef - 1] = true;
/*
* Note: this code is dead so far as core Postgres is concerned,
* because no caller passes add_column_mode = true anymore. We keep
* it in back branches on the slight chance that some extension is
* depending on it.
*/
if (rel->rd_rel->relkind == RELKIND_RELATION && add_column_mode && if (rel->rd_rel->relkind == RELKIND_RELATION && add_column_mode &&
!attgenerated) !attgenerated)
{ {

View File

@ -7078,14 +7078,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault)); rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault));
rawEnt->attnum = attribute.attnum; rawEnt->attnum = attribute.attnum;
rawEnt->raw_default = copyObject(colDef->raw_default); rawEnt->raw_default = copyObject(colDef->raw_default);
rawEnt->missingMode = false; /* XXX vestigial */
/*
* 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);
rawEnt->generated = colDef->generated; rawEnt->generated = colDef->generated;
/* /*
@ -7097,13 +7090,6 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
/* Make the additional catalog changes visible */ /* Make the additional catalog changes visible */
CommandCounterIncrement(); 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;
} }
/* /*
@ -7120,9 +7106,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
* rejects nulls. If there are any domain constraints then we construct * rejects nulls. If there are any domain constraints then we construct
* an explicit NULL default value that will be passed through * an explicit NULL default value that will be passed through
* CoerceToDomain processing. (This is a tad inefficient, since it causes * CoerceToDomain processing. (This is a tad inefficient, since it causes
* rewriting the table which we really don't have to do, but the present * rewriting the table which we really wouldn't have to do; but we do it
* design of domain processing doesn't offer any simple way of checking * to preserve the historical behavior that such a failure will be raised
* the constraints more directly.) * only if the table currently contains some rows.)
* *
* Note: we use build_column_default, and not just the cooked default * Note: we use build_column_default, and not just the cooked default
* returned by AddRelationNewConstraints, so that the right thing happens * returned by AddRelationNewConstraints, so that the right thing happens
@ -7141,6 +7127,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
*/ */
if (RELKIND_HAS_STORAGE(relkind) && attribute.attnum > 0) if (RELKIND_HAS_STORAGE(relkind) && attribute.attnum > 0)
{ {
bool has_domain_constraints;
bool has_missing = false;
/* /*
* For an identity column, we can't use build_column_default(), * For an identity column, we can't use build_column_default(),
* because the sequence ownership isn't set yet. So do it manually. * because the sequence ownership isn't set yet. So do it manually.
@ -7153,14 +7142,13 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
nve->typeId = typeOid; nve->typeId = typeOid;
defval = (Expr *) nve; defval = (Expr *) nve;
/* must do a rewrite for identity columns */
tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
} }
else else
defval = (Expr *) build_column_default(rel, attribute.attnum); defval = (Expr *) build_column_default(rel, attribute.attnum);
if (!defval && DomainHasConstraints(typeOid)) /* Build CoerceToDomain(NULL) expression if needed */
has_domain_constraints = DomainHasConstraints(typeOid);
if (!defval && has_domain_constraints)
{ {
Oid baseTypeId; Oid baseTypeId;
int32 baseTypeMod; int32 baseTypeMod;
@ -7186,18 +7174,61 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
{ {
NewColumnValue *newval; 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 = (NewColumnValue *) palloc0(sizeof(NewColumnValue));
newval->attnum = attribute.attnum; newval->attnum = attribute.attnum;
newval->expr = expression_planner(defval); newval->expr = defval;
newval->is_generated = (colDef->generated != '\0'); newval->is_generated = (colDef->generated != '\0');
tab->newvals = lappend(tab->newvals, newval); tab->newvals = lappend(tab->newvals, newval);
/*
* 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.
*/
tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
}
} }
if (DomainHasConstraints(typeOid)) if (!has_missing)
tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
if (!TupleDescAttr(rel->rd_att, attribute.attnum - 1)->atthasmissing)
{ {
/* /*
* If the new column is NOT NULL, and there is no missing value, * If the new column is NOT NULL, and there is no missing value,

View File

@ -28,7 +28,7 @@ typedef struct RawColumnDefault
{ {
AttrNumber attnum; /* attribute to attach default to */ AttrNumber attnum; /* attribute to attach default to */
Node *raw_default; /* default value (untransformed parse tree) */ Node *raw_default; /* default value (untransformed parse tree) */
bool missingMode; /* true if part of add column processing */ bool missingMode; /* obsolete, no longer used */
char generated; /* attgenerated setting */ char generated; /* attgenerated setting */
} RawColumnDefault; } RawColumnDefault;
@ -115,6 +115,9 @@ extern List *AddRelationNewConstraints(Relation rel,
const char *queryString); const char *queryString);
extern void RelationClearMissing(Relation rel); extern void RelationClearMissing(Relation rel);
extern void StoreAttrMissingVal(Relation rel, AttrNumber attnum,
Datum missingval);
extern void SetAttrMissing(Oid relid, char *attname, char *value); extern void SetAttrMissing(Oid relid, char *attname, char *value);
extern Node *cookDefault(ParseState *pstate, extern Node *cookDefault(ParseState *pstate,

View File

@ -245,6 +245,71 @@ SELECT comp();
(1 row) (1 row)
DROP TABLE T; DROP TABLE T;
-- Test domains with default value for table rewrite.
CREATE DOMAIN domain1 AS int DEFAULT 11; -- constant
CREATE DOMAIN domain2 AS int DEFAULT 10 + (random() * 10)::int; -- volatile
CREATE DOMAIN domain3 AS text DEFAULT foo(4); -- stable
CREATE DOMAIN domain4 AS text[]
DEFAULT ('{"This", "is", "' || foo(4) || '","the", "real", "world"}')::TEXT[];
CREATE TABLE t2 (a domain1);
INSERT INTO t2 VALUES (1),(2);
-- no table rewrite
ALTER TABLE t2 ADD COLUMN b domain1 default 3;
SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
FROM pg_attribute
WHERE attnum > 0 AND attrelid = 't2'::regclass
ORDER BY attnum;
attnum | attname | atthasmissing | atthasdef | attmissingval
--------+---------+---------------+-----------+---------------
1 | a | f | f |
2 | b | t | t | {3}
(2 rows)
-- table rewrite should happen
ALTER TABLE t2 ADD COLUMN c domain3 default left(random()::text,3);
NOTICE: rewriting table t2 for reason 2
-- no table rewrite
ALTER TABLE t2 ADD COLUMN d domain4;
SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
FROM pg_attribute
WHERE attnum > 0 AND attrelid = 't2'::regclass
ORDER BY attnum;
attnum | attname | atthasmissing | atthasdef | attmissingval
--------+---------+---------------+-----------+-----------------------------------
1 | a | f | f |
2 | b | f | t |
3 | c | f | t |
4 | d | t | f | {"{This,is,abcd,the,real,world}"}
(4 rows)
-- table rewrite should happen
ALTER TABLE t2 ADD COLUMN e domain2;
NOTICE: rewriting table t2 for reason 2
SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
FROM pg_attribute
WHERE attnum > 0 AND attrelid = 't2'::regclass
ORDER BY attnum;
attnum | attname | atthasmissing | atthasdef | attmissingval
--------+---------+---------------+-----------+---------------
1 | a | f | f |
2 | b | f | t |
3 | c | f | t |
4 | d | f | f |
5 | e | f | f |
(5 rows)
SELECT a, b, length(c) = 3 as c_ok, d, e >= 10 as e_ok FROM t2;
a | b | c_ok | d | e_ok
---+---+------+-------------------------------+------
1 | 3 | t | {This,is,abcd,the,real,world} | t
2 | 3 | t | {This,is,abcd,the,real,world} | t
(2 rows)
DROP TABLE t2;
DROP DOMAIN domain1;
DROP DOMAIN domain2;
DROP DOMAIN domain3;
DROP DOMAIN domain4;
DROP FUNCTION foo(INT); DROP FUNCTION foo(INT);
-- Fall back to full rewrite for volatile expressions -- Fall back to full rewrite for volatile expressions
CREATE TABLE T(pk INT NOT NULL PRIMARY KEY); CREATE TABLE T(pk INT NOT NULL PRIMARY KEY);

View File

@ -237,6 +237,50 @@ SELECT comp();
DROP TABLE T; DROP TABLE T;
-- Test domains with default value for table rewrite.
CREATE DOMAIN domain1 AS int DEFAULT 11; -- constant
CREATE DOMAIN domain2 AS int DEFAULT 10 + (random() * 10)::int; -- volatile
CREATE DOMAIN domain3 AS text DEFAULT foo(4); -- stable
CREATE DOMAIN domain4 AS text[]
DEFAULT ('{"This", "is", "' || foo(4) || '","the", "real", "world"}')::TEXT[];
CREATE TABLE t2 (a domain1);
INSERT INTO t2 VALUES (1),(2);
-- no table rewrite
ALTER TABLE t2 ADD COLUMN b domain1 default 3;
SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
FROM pg_attribute
WHERE attnum > 0 AND attrelid = 't2'::regclass
ORDER BY attnum;
-- table rewrite should happen
ALTER TABLE t2 ADD COLUMN c domain3 default left(random()::text,3);
-- no table rewrite
ALTER TABLE t2 ADD COLUMN d domain4;
SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
FROM pg_attribute
WHERE attnum > 0 AND attrelid = 't2'::regclass
ORDER BY attnum;
-- table rewrite should happen
ALTER TABLE t2 ADD COLUMN e domain2;
SELECT attnum, attname, atthasmissing, atthasdef, attmissingval
FROM pg_attribute
WHERE attnum > 0 AND attrelid = 't2'::regclass
ORDER BY attnum;
SELECT a, b, length(c) = 3 as c_ok, d, e >= 10 as e_ok FROM t2;
DROP TABLE t2;
DROP DOMAIN domain1;
DROP DOMAIN domain2;
DROP DOMAIN domain3;
DROP DOMAIN domain4;
DROP FUNCTION foo(INT); DROP FUNCTION foo(INT);
-- Fall back to full rewrite for volatile expressions -- Fall back to full rewrite for volatile expressions