diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index bd3554c0bfdc..b807ab666689 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -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 diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 89cc83e88434..33c2106c17ce 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -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")); } /* diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 29ca7c9369cf..fec4ef5d87be 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -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,32 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap) List *dropped_attrs = NIL; ListCell *lc; Snapshot snapshot; + ResultRelInfo *rInfo = NULL; + + /* + * Whether change an existing generation expression or adding a new + * not-null virtual generated column, we need to evaluate whether the + * generation expression is null. + * In ExecConstraints, we use ExecRelCheckGenVirtualNotNull do the job. + * Here, we can also utilize ExecRelCheckGenVirtualNotNull too. + * To achieve this, we need create a dummy ResultRelInfo. Ensure that + * the resultRelationIndex of the dummy ResultRelInfo is set to 0. + */ + 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 +6431,26 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap) } } + if (notnull_virtual_attrs != NIL) + { + AttrNumber attnum; + + attnum = ExecRelCheckGenVirtualNotNull(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 +7900,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 +8568,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 diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index e9bd98c7738c..f90fc001185b 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -51,6 +51,7 @@ #include "foreign/fdwapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/makefuncs.h" #include "nodes/queryjumble.h" #include "parser/parse_relation.h" #include "pgstat.h" @@ -92,7 +93,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 attrChk); /* end of local decls */ @@ -1372,6 +1375,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo, resultRelInfo->ri_FdwState = NULL; resultRelInfo->ri_usesFdwDirectModify = false; resultRelInfo->ri_ConstraintExprs = NULL; + resultRelInfo->ri_GeneratedNotNullExprs = NULL; resultRelInfo->ri_GeneratedExprsI = NULL; resultRelInfo->ri_GeneratedExprsU = NULL; resultRelInfo->ri_projectReturning = NULL; @@ -2058,73 +2062,61 @@ ExecConstraints(ResultRelInfo *resultRelInfo, TupleDesc tupdesc = RelationGetDescr(rel); TupleConstr *constr = tupdesc->constr; Bitmapset *modifiedCols; + List *notnull_virtual_attrs = NIL; + bool virtual_notnull_check = false; Assert(constr); /* we should not be called otherwise */ + /* + * First, go over all the not-null constraints and verify and possibly + * throw errors for all of those that aren't on virtual generated columns. + * (The latter need executor support and a precise list of columns to + * handle, so we accumulate that here and initialize separately.) + */ if (constr->has_not_null) { - int natts = tupdesc->natts; - int attrChk; - - for (attrChk = 1; attrChk <= natts; attrChk++) + for (AttrNumber attnum = 1; attnum <= tupdesc->natts; attnum++) { - Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1); + Form_pg_attribute att = TupleDescAttr(tupdesc, attnum - 1); - if (att->attnotnull && slot_attisnull(slot, attrChk)) - { - char *val_desc; - Relation orig_rel = rel; - TupleDesc orig_tupdesc = RelationGetDescr(rel); + if (att->attnotnull && att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + notnull_virtual_attrs = lappend_int(notnull_virtual_attrs, att->attnum); - /* - * 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) + if (att->attnotnull && slot_attisnull(slot, attnum)) + { + if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) { - 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; + if (!virtual_notnull_check) + virtual_notnull_check = true; + continue; } - 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, attrChk))); + ReportNotNullViolationError(resultRelInfo, slot, estate, attnum); } } } + /* Check not-null constraints on virtual generated column, if any */ + if (virtual_notnull_check) + { + AttrNumber attnum; + + Assert(constr->has_not_null); + Assert(constr->has_generated_virtual); + Assert(notnull_virtual_attrs != NIL); + + attnum = ExecRelCheckGenVirtualNotNull(resultRelInfo, slot, estate, + notnull_virtual_attrs); + if (attnum != InvalidAttrNumber) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, attnum - 1); + + Assert(att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL); + ReportNotNullViolationError(resultRelInfo, slot, estate, att->attnum); + } + } + + /* Last, verify any CHECK constraints */ if (rel->rd_rel->relchecks > 0) { const char *failed; @@ -2134,7 +2126,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; @@ -2176,6 +2173,146 @@ 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 +ExecRelCheckGenVirtualNotNull(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, + EState *estate, List *notnull_virtual_attrs) +{ + Relation rel = resultRelInfo->ri_RelationDesc; + ExprContext *econtext; + MemoryContext oldContext; + + /* + * We implement this by consing up a NullTest node for each virtual + * generated column, which we cache in resultRelInfo, and running those + * through ExecCheck. + */ + if (resultRelInfo->ri_GeneratedNotNullExprs == NULL) + { + int cnt = list_length(notnull_virtual_attrs); + + oldContext = MemoryContextSwitchTo(estate->es_query_cxt); + resultRelInfo->ri_GeneratedNotNullExprs = + (ExprState **) palloc0(cnt * sizeof(ExprState *)); + + 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_GeneratedNotNullExprs[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_GeneratedNotNullExprs[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 attrChk) +{ + 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, attrChk - 1); + + Assert(attrChk > 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, attrChk)); +} + /* * ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs * of the specified kind. diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 896a7f2c59b4..9c1541e1fea9 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -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))); } /* diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 6a1fec889281..7244136019dd 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -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 ExecRelCheckGenVirtualNotNull(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, diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index e42f9f9f9574..ed13d2277151 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -549,6 +549,9 @@ typedef struct ResultRelInfo /* array of constraint-checking expr states */ ExprState **ri_ConstraintExprs; + /* array of virtual generated not null constraint-checking expr states */ + ExprState **ri_GeneratedNotNullExprs; + /* * Arrays of stored generated columns ExprStates for INSERT/UPDATE/MERGE. */ diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out index dc09c85938ea..98108191be16 100644 --- a/src/test/regress/expected/generated_virtual.out +++ b/src/test/regress/expected/generated_virtual.out @@ -664,28 +664,72 @@ 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) 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 support 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 case. +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 message. +INSERT INTO gtestnn_parent VALUES (2, 2, default), (3, 5, default), (14, 12, default); +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 +737,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 +782,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 @@ -1029,7 +1073,7 @@ CREATE TABLE gtest27 ( b int, x int GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL ); -INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11); +INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11), (NULL, NULL); ALTER TABLE gtest27 ALTER COLUMN a TYPE text; -- error ERROR: cannot alter type of a column used by a generated column DETAIL: Column "a" is used by generated column "x". @@ -1047,7 +1091,8 @@ SELECT * FROM gtest27; ---+----+---- 3 | 7 | 20 4 | 11 | 30 -(2 rows) + | | +(3 rows) ALTER TABLE gtest27 ALTER COLUMN x TYPE boolean USING x <> 0; -- error ERROR: cannot specify USING when altering type of generated column @@ -1056,6 +1101,14 @@ 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 +--not null error violation +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: 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, diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql index dab8c92ef992..6624d0b22eb8 100644 --- a/src/test/regress/sql/generated_virtual.sql +++ b/src/test/regress/sql/generated_virtual.sql @@ -335,23 +335,50 @@ 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) 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 support 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 case. +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 message. +INSERT INTO gtestnn_parent VALUES (2, 2, default), (3, 5, default), (14, 12, default); +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); @@ -524,13 +551,20 @@ CREATE TABLE gtest27 ( b int, x int GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL ); -INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11); +INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11), (NULL, NULL); ALTER TABLE gtest27 ALTER COLUMN a TYPE text; -- error ALTER TABLE gtest27 ALTER COLUMN x TYPE numeric; \d gtest27 SELECT * FROM gtest27; ALTER TABLE gtest27 ALTER COLUMN x TYPE boolean USING x <> 0; -- error ALTER TABLE gtest27 ALTER COLUMN x DROP DEFAULT; -- error +--not null error violation +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; +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,