Add support for not-null constraints on virtual generated columns
authorPeter Eisentraut <peter@eisentraut.org>
Fri, 28 Mar 2025 12:53:37 +0000 (13:53 +0100)
committerPeter Eisentraut <peter@eisentraut.org>
Fri, 28 Mar 2025 12:53:37 +0000 (13:53 +0100)
This was left out of the original patch for virtual generated columns
(commit 83ea6c54025).

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

src/backend/catalog/heap.c
src/backend/commands/indexcmds.c
src/backend/commands/tablecmds.c
src/backend/executor/execMain.c
src/backend/parser/parse_utilcmd.c
src/include/executor/executor.h
src/include/nodes/execnodes.h
src/test/regress/expected/generated_virtual.out
src/test/regress/sql/generated_virtual.sql

index bd3554c0bfdcf379516668178c0e9e8997f50c74..b807ab666689f92362977a649a06492235fd847a 100644 (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
index 89cc83e884340a133352ce4162a8be5ce40cddc4..33c2106c17cee4f05dd97fcf579765b2b8541ff2 100644 (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"));
    }
 
    /*
index afb250076136fd6e0afbf8cd6012edeff0b4bcff..10624353b0a06b51a0706876992451753e527abb 100644 (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
index a22712755713087a8a519e0a4786e4d3753b3d7c..2da848970be0ecbebe13f36a7ec8303b7f61d33c 100644 (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,59 +2076,24 @@ 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);
+           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);
+       }
+   }
 
-                   /*
-                    * 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);
+   /*
+    * Verify not-null constraints on virtual generated column, if any.
+    */
+   if (notnull_virtual_attrs)
+   {
+       AttrNumber  attnum;
 
-               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));
-           }
-       }
+       attnum = ExecRelGenVirtualNotNull(resultRelInfo, slot, estate,
+                                         notnull_virtual_attrs);
+       if (attnum != InvalidAttrNumber)
+           ReportNotNullViolationError(resultRelInfo, slot, estate, attnum);
    }
 
    /*
@@ -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.
index 896a7f2c59b408edaea6d4191784d3c53a350e30..9c1541e1fea93bdae3b8c1af55182754633e43c4 100644 (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)));
    }
 
    /*
index 6a1fec88928114c525e7e2b74e0bd158816d507d..ae99407db89df2b9d35140c36b34d9591aff9275 100644 (file)
@@ -257,6 +257,10 @@ extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
 extern List *ExecGetAncestorResultRels(EState *estate, ResultRelInfo *resultRelInfo);
 extern void ExecConstraints(ResultRelInfo *resultRelInfo,
                            TupleTableSlot *slot, EState *estate);
+extern AttrNumber ExecRelGenVirtualNotNull(ResultRelInfo *resultRelInfo,
+                                          TupleTableSlot *slot,
+                                          EState *estate,
+                                          List *notnull_virtual_attrs);
 extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
                               TupleTableSlot *slot, EState *estate, bool emitError);
 extern void ExecPartitionCheckEmitError(ResultRelInfo *resultRelInfo,
index bc06fa6f5ea9070d3fc5945a684d5e0c9c24001f..5b6cadb5a6c163ae7eca0dc345a5a151380bba99 100644 (file)
@@ -549,6 +549,12 @@ typedef struct ResultRelInfo
    /* array of expr states for checking check constraints */
    ExprState **ri_CheckConstraintExprs;
 
+   /*
+    * array of expr states for checking not-null constraints on virtual
+    * generated columns
+    */
+   ExprState **ri_GenVirtualNotNullConstraintExprs;
+
    /*
     * Arrays of stored generated columns ExprStates for INSERT/UPDATE/MERGE.
     */
index dc09c85938ea513533dee7cce29fef4f23b3b360..26bbe1e9c317371c9191e85a7de66ea0ec71fdfe 100644 (file)
@@ -664,28 +664,73 @@ INSERT INTO gtest20c VALUES (1);  -- ok
 INSERT INTO gtest20c VALUES (NULL);  -- fails
 ERROR:  new row for relation "gtest20c" violates check constraint "whole_row_check"
 DETAIL:  Failing row contains (null, virtual).
--- not-null constraints (currently not supported)
+-- not-null constraints
 CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL);
-ERROR:  not-null constraints are not supported on virtual generated columns
-LINE 1: ... b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL);
-                                                             ^
---INSERT INTO gtest21a (a) VALUES (1);  -- ok
---INSERT INTO gtest21a (a) VALUES (0);  -- violates constraint
+INSERT INTO gtest21a (a) VALUES (1);  -- ok
+INSERT INTO gtest21a (a) VALUES (0);  -- violates constraint
+ERROR:  null value in column "b" of relation "gtest21a" violates not-null constraint
+DETAIL:  Failing row contains (0, virtual).
 -- also check with table constraint syntax
-CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL, CONSTRAINT cc NOT NULL b);  -- error
-ERROR:  not-null constraints are not supported on virtual generated columns
+CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL, CONSTRAINT cc NOT NULL b);
+INSERT INTO gtest21ax (a) VALUES (0);  -- violates constraint
+ERROR:  null value in column "b" of relation "gtest21ax" violates not-null constraint
+DETAIL:  Failing row contains (0, virtual).
+INSERT INTO gtest21ax (a) VALUES (1);  --ok
+-- SET EXPRESSION supports not null constraint
+ALTER TABLE gtest21ax ALTER COLUMN b SET EXPRESSION AS (nullif(a, 1)); --error
+ERROR:  column "b" of relation "gtest21ax" contains null values
+DROP TABLE gtest21ax;
 CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
-ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;  -- error
-ERROR:  not-null constraints are not supported on virtual generated columns
+ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;
+INSERT INTO gtest21ax (a) VALUES (0);  -- violates constraint
+ERROR:  null value in column "b" of relation "gtest21ax" violates not-null constraint
+DETAIL:  Failing row contains (0, virtual).
 DROP TABLE gtest21ax;
-CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
+CREATE TABLE gtest21b (a int, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
 ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL;
-ERROR:  not-null constraints are not supported on virtual generated columns
-DETAIL:  Column "b" of relation "gtest21b" is a virtual generated column.
---INSERT INTO gtest21b (a) VALUES (1);  -- ok
---INSERT INTO gtest21b (a) VALUES (0);  -- violates constraint
+INSERT INTO gtest21b (a) VALUES (1);  -- ok
+INSERT INTO gtest21b (a) VALUES (2), (0);  -- violates constraint
+ERROR:  null value in column "b" of relation "gtest21b" violates not-null constraint
+DETAIL:  Failing row contains (0, virtual).
+INSERT INTO gtest21b (a) VALUES (NULL);  -- error
+ERROR:  null value in column "b" of relation "gtest21b" violates not-null constraint
+DETAIL:  Failing row contains (null, virtual).
 ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
---INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+-- not-null constraint with partitioned table
+CREATE TABLE gtestnn_parent (
+    f1 int,
+    f2 bigint,
+    f3 bigint GENERATED ALWAYS AS (nullif(f1, 1) + nullif(f2, 10)) VIRTUAL NOT NULL
+) PARTITION BY RANGE (f1);
+CREATE TABLE gtestnn_child PARTITION OF gtestnn_parent FOR VALUES FROM (1) TO (5);
+CREATE TABLE gtestnn_childdef PARTITION OF gtestnn_parent default;
+-- check the error messages
+INSERT INTO gtestnn_parent VALUES (2, 2, default), (3, 5, default), (14, 12, default);  -- ok
+INSERT INTO gtestnn_parent VALUES (1, 2, default);  -- error
+ERROR:  null value in column "f3" of relation "gtestnn_child" violates not-null constraint
+DETAIL:  Failing row contains (1, 2, virtual).
+INSERT INTO gtestnn_parent VALUES (2, 10, default);  -- error
+ERROR:  null value in column "f3" of relation "gtestnn_child" violates not-null constraint
+DETAIL:  Failing row contains (2, 10, virtual).
+ALTER TABLE gtestnn_parent ALTER COLUMN f3 SET EXPRESSION AS (nullif(f1, 2) + nullif(f2, 11));  -- error
+ERROR:  column "f3" of relation "gtestnn_child" contains null values
+INSERT INTO gtestnn_parent VALUES (10, 11, default);  -- ok
+SELECT * FROM gtestnn_parent ORDER BY f1, f2, f3;
+ f1 | f2 | f3 
+----+----+----
+  2 |  2 |  4
+  3 |  5 |  8
+ 10 | 11 | 21
+ 14 | 12 | 26
+(4 rows)
+
+-- test ALTER TABLE ADD COLUMN
+ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 14) + nullif(f2, 10)) VIRTUAL;  -- error
+ERROR:  column "c" of relation "gtestnn_childdef" contains null values
+ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 13) + nullif(f2, 5)) VIRTUAL;  -- error
+ERROR:  column "c" of relation "gtestnn_child" contains null values
+ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 4) + nullif(f2, 6)) VIRTUAL;  -- ok
 -- index constraints
 CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) VIRTUAL UNIQUE);
 ERROR:  unique constraints on virtual generated columns are not supported
@@ -693,7 +738,7 @@ ERROR:  unique constraints on virtual generated columns are not supported
 --INSERT INTO gtest22a VALUES (3);
 --INSERT INTO gtest22a VALUES (4);
 CREATE TABLE gtest22b (a int, b int GENERATED ALWAYS AS (a / 2) VIRTUAL, PRIMARY KEY (a, b));
-ERROR:  not-null constraints are not supported on virtual generated columns
+ERROR:  primary keys on virtual generated columns are not supported
 --INSERT INTO gtest22b VALUES (2);
 --INSERT INTO gtest22b VALUES (2);
 -- indexes
@@ -738,7 +783,7 @@ ERROR:  foreign key constraints on virtual generated columns are not supported
 --DROP TABLE gtest23b;
 --DROP TABLE gtest23a;
 CREATE TABLE gtest23p (x int, y int GENERATED ALWAYS AS (x * 2) VIRTUAL, PRIMARY KEY (y));
-ERROR:  not-null constraints are not supported on virtual generated columns
+ERROR:  primary keys on virtual generated columns are not supported
 --INSERT INTO gtest23p VALUES (1), (2), (3);
 CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y));
 ERROR:  relation "gtest23p" does not exist
@@ -1056,6 +1101,15 @@ LINE 1: ALTER TABLE gtest27 ALTER COLUMN x TYPE boolean USING x <> 0...
 DETAIL:  Column "x" is a generated column.
 ALTER TABLE gtest27 ALTER COLUMN x DROP DEFAULT;  -- error
 ERROR:  column "x" of relation "gtest27" is a generated column
+-- test not-null checking during table rewrite
+INSERT INTO gtest27 (a, b) VALUES (NULL, NULL);
+ALTER TABLE gtest27
+  DROP COLUMN x,
+  ALTER COLUMN a TYPE bigint,
+  ALTER COLUMN b TYPE bigint,
+  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL NOT NULL;  -- error
+ERROR:  column "x" of relation "gtest27" contains null values
+DELETE FROM gtest27 WHERE a IS NULL AND b IS NULL;
 -- It's possible to alter the column types this way:
 ALTER TABLE gtest27
   DROP COLUMN x,
index dab8c92ef99288f0f41e830f49c1bb3b32e1dfde..13cfbd768594d4ed379c7ae42d2bf45287077a3f 100644 (file)
@@ -335,23 +335,51 @@ ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL)
 INSERT INTO gtest20c VALUES (1);  -- ok
 INSERT INTO gtest20c VALUES (NULL);  -- fails
 
--- not-null constraints (currently not supported)
+-- not-null constraints
 CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL);
---INSERT INTO gtest21a (a) VALUES (1);  -- ok
---INSERT INTO gtest21a (a) VALUES (0);  -- violates constraint
+INSERT INTO gtest21a (a) VALUES (1);  -- ok
+INSERT INTO gtest21a (a) VALUES (0);  -- violates constraint
 
 -- also check with table constraint syntax
-CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL, CONSTRAINT cc NOT NULL b);  -- error
+CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL, CONSTRAINT cc NOT NULL b);
+INSERT INTO gtest21ax (a) VALUES (0);  -- violates constraint
+INSERT INTO gtest21ax (a) VALUES (1);  --ok
+-- SET EXPRESSION supports not null constraint
+ALTER TABLE gtest21ax ALTER COLUMN b SET EXPRESSION AS (nullif(a, 1)); --error
+DROP TABLE gtest21ax;
+
 CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
-ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;  -- error
+ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;
+INSERT INTO gtest21ax (a) VALUES (0);  -- violates constraint
 DROP TABLE gtest21ax;
 
-CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
+CREATE TABLE gtest21b (a int, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
 ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL;
---INSERT INTO gtest21b (a) VALUES (1);  -- ok
---INSERT INTO gtest21b (a) VALUES (0);  -- violates constraint
+INSERT INTO gtest21b (a) VALUES (1);  -- ok
+INSERT INTO gtest21b (a) VALUES (2), (0);  -- violates constraint
+INSERT INTO gtest21b (a) VALUES (NULL);  -- error
 ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
---INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+
+-- not-null constraint with partitioned table
+CREATE TABLE gtestnn_parent (
+    f1 int,
+    f2 bigint,
+    f3 bigint GENERATED ALWAYS AS (nullif(f1, 1) + nullif(f2, 10)) VIRTUAL NOT NULL
+) PARTITION BY RANGE (f1);
+CREATE TABLE gtestnn_child PARTITION OF gtestnn_parent FOR VALUES FROM (1) TO (5);
+CREATE TABLE gtestnn_childdef PARTITION OF gtestnn_parent default;
+-- check the error messages
+INSERT INTO gtestnn_parent VALUES (2, 2, default), (3, 5, default), (14, 12, default);  -- ok
+INSERT INTO gtestnn_parent VALUES (1, 2, default);  -- error
+INSERT INTO gtestnn_parent VALUES (2, 10, default);  -- error
+ALTER TABLE gtestnn_parent ALTER COLUMN f3 SET EXPRESSION AS (nullif(f1, 2) + nullif(f2, 11));  -- error
+INSERT INTO gtestnn_parent VALUES (10, 11, default);  -- ok
+SELECT * FROM gtestnn_parent ORDER BY f1, f2, f3;
+-- test ALTER TABLE ADD COLUMN
+ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 14) + nullif(f2, 10)) VIRTUAL;  -- error
+ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 13) + nullif(f2, 5)) VIRTUAL;  -- error
+ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 4) + nullif(f2, 6)) VIRTUAL;  -- ok
 
 -- index constraints
 CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) VIRTUAL UNIQUE);
@@ -531,6 +559,14 @@ ALTER TABLE gtest27 ALTER COLUMN x TYPE numeric;
 SELECT * FROM gtest27;
 ALTER TABLE gtest27 ALTER COLUMN x TYPE boolean USING x <> 0;  -- error
 ALTER TABLE gtest27 ALTER COLUMN x DROP DEFAULT;  -- error
+-- test not-null checking during table rewrite
+INSERT INTO gtest27 (a, b) VALUES (NULL, NULL);
+ALTER TABLE gtest27
+  DROP COLUMN x,
+  ALTER COLUMN a TYPE bigint,
+  ALTER COLUMN b TYPE bigint,
+  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL NOT NULL;  -- error
+DELETE FROM gtest27 WHERE a IS NULL AND b IS NULL;
 -- It's possible to alter the column types this way:
 ALTER TABLE gtest27
   DROP COLUMN x,