1
0
mirror of https://github.com/postgres/postgres.git synced 2025-11-19 13:42:17 +03:00

Add support for not-null constraints on virtual generated columns

This was left out of the original patch for virtual generated columns
(commit 83ea6c5402).

This just involves a bit of extra work in the executor to expand the
generation expressions and run a "IS NOT NULL" test against them.

There is also a bit of work to make sure that not-null constraints are
checked during a table rewrite.

Author: jian he <jian.universality@gmail.com>
Reviewed-by: Xuneng Zhou <xunengzhou@gmail.com>
Reviewed-by: Navneet Kumar <thanit3111@gmail.com>
Reviewed-by: Álvaro Herrera <alvherre@alvh.no-ip.org>
Discussion: https://postgr.es/m/CACJufxHArQysbDkWFmvK+D1TPHQWWTxWN15cMuUaTYX3xhQXgg@mail.gmail.com
This commit is contained in:
Peter Eisentraut
2025-03-28 13:53:37 +01:00
parent 747ddd38cb
commit cdc168ad4b
9 changed files with 363 additions and 122 deletions

View File

@@ -2615,11 +2615,6 @@ AddRelationNewConstraints(Relation rel,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot add not-null constraint on system column \"%s\"",
strVal(linitial(cdef->keys))));
/* TODO: see transformColumnDefinition() */
if (get_attgenerated(RelationGetRelid(rel), colnum) == ATTRIBUTE_GENERATED_VIRTUAL)
ereport(ERROR,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("not-null constraints are not supported on virtual generated columns"));
/*
* If the column already has a not-null constraint, we don't want
@@ -2935,11 +2930,6 @@ AddRelationNotNullConstraints(Relation rel, List *constraints,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot add not-null constraint on system column \"%s\"",
strVal(linitial(constr->keys))));
/* TODO: see transformColumnDefinition() */
if (get_attgenerated(RelationGetRelid(rel), attnum) == ATTRIBUTE_GENERATED_VIRTUAL)
ereport(ERROR,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("not-null constraints are not supported on virtual generated columns"));
/*
* A column can only have one not-null constraint, so discard any

View File

@@ -1118,10 +1118,12 @@ DefineIndex(Oid tableId,
if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
stmt->isconstraint ?
errmsg("unique constraints on virtual generated columns are not supported") :
errmsg("indexes on virtual generated columns are not supported")));
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
stmt->primary ?
errmsg("primary keys on virtual generated columns are not supported") :
stmt->isconstraint ?
errmsg("unique constraints on virtual generated columns are not supported") :
errmsg("indexes on virtual generated columns are not supported"));
}
/*

View File

@@ -6101,6 +6101,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
TupleDesc newTupDesc;
bool needscan = false;
List *notnull_attrs;
List *notnull_virtual_attrs;
int i;
ListCell *l;
EState *estate;
@@ -6185,22 +6186,32 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL);
}
notnull_attrs = NIL;
notnull_attrs = notnull_virtual_attrs = NIL;
if (newrel || tab->verify_new_notnull)
{
/*
* If we are rebuilding the tuples OR if we added any new but not
* verified not-null constraints, check all not-null constraints. This
* is a bit of overkill but it minimizes risk of bugs.
*
* notnull_attrs does *not* collect attribute numbers for not-null
* constraints over virtual generated columns; instead, they are
* collected in notnull_virtual_attrs.
*/
for (i = 0; i < newTupDesc->natts; i++)
{
Form_pg_attribute attr = TupleDescAttr(newTupDesc, i);
if (attr->attnotnull && !attr->attisdropped)
notnull_attrs = lappend_int(notnull_attrs, attr->attnum);
{
if (attr->attgenerated != ATTRIBUTE_GENERATED_VIRTUAL)
notnull_attrs = lappend_int(notnull_attrs, attr->attnum);
else
notnull_virtual_attrs = lappend_int(notnull_virtual_attrs,
attr->attnum);
}
}
if (notnull_attrs)
if (notnull_attrs || notnull_virtual_attrs)
needscan = true;
}
@@ -6214,6 +6225,29 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
List *dropped_attrs = NIL;
ListCell *lc;
Snapshot snapshot;
ResultRelInfo *rInfo = NULL;
/*
* When adding or changing a virtual generated column with a not-null
* constraint, we need to evaluate whether the generation expression
* is null. For that, we borrow ExecRelGenVirtualNotNull(). Here, we
* prepare a dummy ResultRelInfo.
*/
if (notnull_virtual_attrs != NIL)
{
MemoryContext oldcontext;
Assert(newTupDesc->constr->has_generated_virtual);
Assert(newTupDesc->constr->has_not_null);
oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
rInfo = makeNode(ResultRelInfo);
InitResultRelInfo(rInfo,
oldrel,
0, /* dummy rangetable index */
NULL,
estate->es_instrument);
MemoryContextSwitchTo(oldcontext);
}
if (newrel)
ereport(DEBUG1,
@@ -6394,6 +6428,26 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
}
}
if (notnull_virtual_attrs != NIL)
{
AttrNumber attnum;
attnum = ExecRelGenVirtualNotNull(rInfo, insertslot,
estate,
notnull_virtual_attrs);
if (attnum != InvalidAttrNumber)
{
Form_pg_attribute attr = TupleDescAttr(newTupDesc, attnum - 1);
ereport(ERROR,
errcode(ERRCODE_NOT_NULL_VIOLATION),
errmsg("column \"%s\" of relation \"%s\" contains null values",
NameStr(attr->attname),
RelationGetRelationName(oldrel)),
errtablecol(oldrel, attnum));
}
}
foreach(l, tab->constraints)
{
NewConstraint *con = lfirst(l);
@@ -7843,14 +7897,6 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
errmsg("cannot alter system column \"%s\"",
colName)));
/* TODO: see transformColumnDefinition() */
if (TupleDescAttr(RelationGetDescr(rel), attnum - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("not-null constraints are not supported on virtual generated columns"),
errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
colName, RelationGetRelationName(rel))));
/* See if there's already a constraint */
tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
if (HeapTupleIsValid(tuple))
@@ -8519,6 +8565,9 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
colName, RelationGetRelationName(rel))));
if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL && attTup->attnotnull)
tab->verify_new_notnull = true;
/*
* We need to prevent this because a change of expression could affect a
* row filter and inject expressions that are not permitted in a row

View File

@@ -92,6 +92,9 @@ static bool ExecCheckPermissionsModified(Oid relOid, Oid userid,
AclMode requiredPerms);
static void ExecCheckXactReadOnly(PlannedStmt *plannedstmt);
static void EvalPlanQualStart(EPQState *epqstate, Plan *planTree);
static void ReportNotNullViolationError(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot,
EState *estate, int attnum);
/* end of local decls */
@@ -1372,6 +1375,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_FdwState = NULL;
resultRelInfo->ri_usesFdwDirectModify = false;
resultRelInfo->ri_CheckConstraintExprs = NULL;
resultRelInfo->ri_GenVirtualNotNullConstraintExprs = NULL;
resultRelInfo->ri_GeneratedExprsI = NULL;
resultRelInfo->ri_GeneratedExprsU = NULL;
resultRelInfo->ri_projectReturning = NULL;
@@ -1842,7 +1846,7 @@ ExecutePlan(QueryDesc *queryDesc,
/*
* ExecRelCheck --- check that tuple meets constraints for result relation
* ExecRelCheck --- check that tuple meets check constraints for result relation
*
* Returns NULL if OK, else name of failed check constraint
*/
@@ -2056,11 +2060,15 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
TupleDesc tupdesc = RelationGetDescr(rel);
TupleConstr *constr = tupdesc->constr;
Bitmapset *modifiedCols;
List *notnull_virtual_attrs = NIL;
Assert(constr); /* we should not be called otherwise */
/*
* Verify not-null constraints.
*
* Not-null constraints on virtual generated columns are collected and
* checked separately below.
*/
if (constr->has_not_null)
{
@@ -2068,61 +2076,26 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
{
Form_pg_attribute att = TupleDescAttr(tupdesc, attnum - 1);
if (att->attnotnull && slot_attisnull(slot, attnum))
{
char *val_desc;
Relation orig_rel = rel;
TupleDesc orig_tupdesc = RelationGetDescr(rel);
/*
* If the tuple has been routed, it's been converted to the
* partition's rowtype, which might differ from the root
* table's. We must convert it back to the root table's
* rowtype so that val_desc shown error message matches the
* input tuple.
*/
if (resultRelInfo->ri_RootResultRelInfo)
{
ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
AttrMap *map;
tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
/* a reverse map */
map = build_attrmap_by_name_if_req(orig_tupdesc,
tupdesc,
false);
/*
* Partition-specific slot's tupdesc can't be changed, so
* allocate a new one.
*/
if (map != NULL)
slot = execute_attr_map_slot(map, slot,
MakeTupleTableSlot(tupdesc, &TTSOpsVirtual));
modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate),
ExecGetUpdatedCols(rootrel, estate));
rel = rootrel->ri_RelationDesc;
}
else
modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, estate),
ExecGetUpdatedCols(resultRelInfo, estate));
val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
slot,
tupdesc,
modifiedCols,
64);
ereport(ERROR,
errcode(ERRCODE_NOT_NULL_VIOLATION),
errmsg("null value in column \"%s\" of relation \"%s\" violates not-null constraint",
NameStr(att->attname),
RelationGetRelationName(orig_rel)),
val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
errtablecol(orig_rel, attnum));
}
if (att->attnotnull && att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
notnull_virtual_attrs = lappend_int(notnull_virtual_attrs, attnum);
else if (att->attnotnull && slot_attisnull(slot, attnum))
ReportNotNullViolationError(resultRelInfo, slot, estate, attnum);
}
}
/*
* Verify not-null constraints on virtual generated column, if any.
*/
if (notnull_virtual_attrs)
{
AttrNumber attnum;
attnum = ExecRelGenVirtualNotNull(resultRelInfo, slot, estate,
notnull_virtual_attrs);
if (attnum != InvalidAttrNumber)
ReportNotNullViolationError(resultRelInfo, slot, estate, attnum);
}
/*
* Verify check constraints.
*/
@@ -2135,7 +2108,12 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
char *val_desc;
Relation orig_rel = rel;
/* See the comment above. */
/*
* If the tuple has been routed, it's been converted to the
* partition's rowtype, which might differ from the root table's.
* We must convert it back to the root table's rowtype so that
* val_desc shown error message matches the input tuple.
*/
if (resultRelInfo->ri_RootResultRelInfo)
{
ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
@@ -2177,6 +2155,142 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
}
}
/*
* Verify not-null constraints on virtual generated columns of the given
* tuple slot.
*
* Return value of InvalidAttrNumber means all not-null constraints on virtual
* generated columns are satisfied. A return value > 0 means a not-null
* violation happened for that attribute.
*
* notnull_virtual_attrs is the list of the attnums of virtual generated column with
* not-null constraints.
*/
AttrNumber
ExecRelGenVirtualNotNull(ResultRelInfo *resultRelInfo, TupleTableSlot *slot,
EState *estate, List *notnull_virtual_attrs)
{
Relation rel = resultRelInfo->ri_RelationDesc;
ExprContext *econtext;
MemoryContext oldContext;
/*
* We implement this by building a NullTest node for each virtual
* generated column, which we cache in resultRelInfo, and running those
* through ExecCheck().
*/
if (resultRelInfo->ri_GenVirtualNotNullConstraintExprs == NULL)
{
oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
resultRelInfo->ri_GenVirtualNotNullConstraintExprs =
palloc0_array(ExprState *, list_length(notnull_virtual_attrs));
foreach_int(attnum, notnull_virtual_attrs)
{
int i = foreach_current_index(attnum);
NullTest *nnulltest;
/* "generated_expression IS NOT NULL" check. */
nnulltest = makeNode(NullTest);
nnulltest->arg = (Expr *) build_generation_expression(rel, attnum);
nnulltest->nulltesttype = IS_NOT_NULL;
nnulltest->argisrow = false;
nnulltest->location = -1;
resultRelInfo->ri_GenVirtualNotNullConstraintExprs[i] =
ExecPrepareExpr((Expr *) nnulltest, estate);
}
MemoryContextSwitchTo(oldContext);
}
/*
* We will use the EState's per-tuple context for evaluating virtual
* generated column not null constraint expressions (creating it if it's
* not already there).
*/
econtext = GetPerTupleExprContext(estate);
/* Arrange for econtext's scan tuple to be the tuple under test */
econtext->ecxt_scantuple = slot;
/* And evaluate the check constraints for virtual generated column */
foreach_int(attnum, notnull_virtual_attrs)
{
int i = foreach_current_index(attnum);
ExprState *exprstate = resultRelInfo->ri_GenVirtualNotNullConstraintExprs[i];
Assert(exprstate != NULL);
if (!ExecCheck(exprstate, econtext))
return attnum;
}
/* InvalidAttrNumber result means no error */
return InvalidAttrNumber;
}
/*
* Report a violation of a not-null constraint that was already detected.
*/
static void
ReportNotNullViolationError(ResultRelInfo *resultRelInfo, TupleTableSlot *slot,
EState *estate, int attnum)
{
Bitmapset *modifiedCols;
char *val_desc;
Relation rel = resultRelInfo->ri_RelationDesc;
Relation orig_rel = rel;
TupleDesc tupdesc = RelationGetDescr(rel);
TupleDesc orig_tupdesc = RelationGetDescr(rel);
Form_pg_attribute att = TupleDescAttr(tupdesc, attnum - 1);
Assert(attnum > 0);
/*
* If the tuple has been routed, it's been converted to the partition's
* rowtype, which might differ from the root table's. We must convert it
* back to the root table's rowtype so that val_desc shown error message
* matches the input tuple.
*/
if (resultRelInfo->ri_RootResultRelInfo)
{
ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
AttrMap *map;
tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
/* a reverse map */
map = build_attrmap_by_name_if_req(orig_tupdesc,
tupdesc,
false);
/*
* Partition-specific slot's tupdesc can't be changed, so allocate a
* new one.
*/
if (map != NULL)
slot = execute_attr_map_slot(map, slot,
MakeTupleTableSlot(tupdesc, &TTSOpsVirtual));
modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate),
ExecGetUpdatedCols(rootrel, estate));
rel = rootrel->ri_RelationDesc;
}
else
modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, estate),
ExecGetUpdatedCols(resultRelInfo, estate));
val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
slot,
tupdesc,
modifiedCols,
64);
ereport(ERROR,
errcode(ERRCODE_NOT_NULL_VIOLATION),
errmsg("null value in column \"%s\" of relation \"%s\" violates not-null constraint",
NameStr(att->attname),
RelationGetRelationName(orig_rel)),
val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
errtablecol(orig_rel, attnum));
}
/*
* ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
* of the specified kind.

View File

@@ -988,20 +988,6 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
column->colname, cxt->relation->relname),
parser_errposition(cxt->pstate,
constraint->location)));
/*
* TODO: Straightforward not-null constraints won't work on virtual
* generated columns, because there is no support for expanding the
* column when the constraint is checked. Maybe we could convert the
* not-null constraint into a full check constraint, so that the
* generation expression can be expanded at check time.
*/
if (column->is_not_null && column->generated == ATTRIBUTE_GENERATED_VIRTUAL)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("not-null constraints are not supported on virtual generated columns"),
parser_errposition(cxt->pstate,
constraint->location)));
}
/*