aboutsummaryrefslogtreecommitdiff
path: root/src/backend
diff options
context:
space:
mode:
authorPeter Eisentraut <peter@eisentraut.org>2025-02-07 09:09:34 +0100
committerPeter Eisentraut <peter@eisentraut.org>2025-02-07 09:46:59 +0100
commit83ea6c54025bea67bcd4949a6d58d3fc11c3e21b (patch)
tree5a5d13a9f27cd08958d821656086dd1c054516f5 /src/backend
parentcbc127917e04a978a788b8bc9d35a70244396d5b (diff)
downloadpostgresql-83ea6c54025bea67bcd4949a6d58d3fc11c3e21b.tar.gz
postgresql-83ea6c54025bea67bcd4949a6d58d3fc11c3e21b.zip
Virtual generated columns
This adds a new variant of generated columns that are computed on read (like a view, unlike the existing stored generated columns, which are computed on write, like a materialized view). The syntax for the column definition is ... GENERATED ALWAYS AS (...) VIRTUAL and VIRTUAL is also optional. VIRTUAL is the default rather than STORED to match various other SQL products. (The SQL standard makes no specification about this, but it also doesn't know about VIRTUAL or STORED.) (Also, virtual views are the default, rather than materialized views.) Virtual generated columns are stored in tuples as null values. (A very early version of this patch had the ambition to not store them at all. But so much stuff breaks or gets confused if you have tuples where a column in the middle is completely missing. This is a compromise, and it still saves space over being forced to use stored generated columns. If we ever find a way to improve this, a bit of pg_upgrade cleverness could allow for upgrades to a newer scheme.) The capabilities and restrictions of virtual generated columns are mostly the same as for stored generated columns. In some cases, this patch keeps virtual generated columns more restricted than they might technically need to be, to keep the two kinds consistent. Some of that could maybe be relaxed later after separate careful considerations. Some functionality that is currently not supported, but could possibly be added as incremental features, some easier than others: - index on or using a virtual column - hence also no unique constraints on virtual columns - extended statistics on virtual columns - foreign-key constraints on virtual columns - not-null constraints on virtual columns (check constraints are supported) - ALTER TABLE / DROP EXPRESSION - virtual column cannot have domain type - virtual columns are not supported in logical replication The tests in generated_virtual.sql have been copied over from generated_stored.sql with the keyword replaced. This way we can make sure the behavior is mostly aligned, and the differences can be visible. Some tests for currently not supported features are currently commented out. Reviewed-by: Jian He <jian.universality@gmail.com> Reviewed-by: Dean Rasheed <dean.a.rasheed@gmail.com> Tested-by: Shlok Kyal <shlok.kyal.oss@gmail.com> Discussion: https://www.postgresql.org/message-id/flat/a368248e-69e4-40be-9c07-6c3b5880b0a6@eisentraut.org
Diffstat (limited to 'src/backend')
-rw-r--r--src/backend/access/common/tupdesc.c3
-rw-r--r--src/backend/access/heap/heapam_handler.c2
-rw-r--r--src/backend/catalog/heap.c23
-rw-r--r--src/backend/catalog/pg_publication.c10
-rw-r--r--src/backend/commands/analyze.c4
-rw-r--r--src/backend/commands/indexcmds.c33
-rw-r--r--src/backend/commands/publicationcmds.c23
-rw-r--r--src/backend/commands/statscmds.c20
-rw-r--r--src/backend/commands/tablecmds.c196
-rw-r--r--src/backend/commands/trigger.c45
-rw-r--r--src/backend/executor/execExprInterp.c4
-rw-r--r--src/backend/executor/execMain.c5
-rw-r--r--src/backend/executor/execUtils.c4
-rw-r--r--src/backend/executor/nodeModifyTable.c41
-rw-r--r--src/backend/parser/gram.y15
-rw-r--r--src/backend/parser/parse_relation.c6
-rw-r--r--src/backend/parser/parse_utilcmd.c16
-rw-r--r--src/backend/replication/pgoutput/pgoutput.c3
-rw-r--r--src/backend/rewrite/rewriteHandler.c133
-rw-r--r--src/backend/utils/cache/relcache.c3
20 files changed, 519 insertions, 70 deletions
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index fe197447912..ed2195f14b2 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -342,6 +342,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
cpy->has_not_null = constr->has_not_null;
cpy->has_generated_stored = constr->has_generated_stored;
+ cpy->has_generated_virtual = constr->has_generated_virtual;
if ((cpy->num_defval = constr->num_defval) > 0)
{
@@ -640,6 +641,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
return false;
if (constr1->has_generated_stored != constr2->has_generated_stored)
return false;
+ if (constr1->has_generated_virtual != constr2->has_generated_virtual)
+ return false;
n = constr1->num_defval;
if (n != (int) constr2->num_defval)
return false;
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index a4003cf59e1..c0bec014154 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -2047,6 +2047,8 @@ heapam_relation_needs_toast_table(Relation rel)
if (att->attisdropped)
continue;
+ if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ continue;
data_length = att_align_nominal(data_length, att->attalign);
if (att->attlen > 0)
{
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 57ef466acce..956f196fc95 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -507,7 +507,7 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
TupleDescAttr(tupdesc, i)->atttypid,
TupleDescAttr(tupdesc, i)->attcollation,
NIL, /* assume we're creating a new rowtype */
- flags);
+ flags | (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ? CHKATYPE_IS_VIRTUAL : 0));
}
}
@@ -583,6 +583,17 @@ CheckAttributeType(const char *attname,
else if (att_typtype == TYPTYPE_DOMAIN)
{
/*
+ * Prevent virtual generated columns from having a domain type. We
+ * would have to enforce domain constraints when columns underlying
+ * the generated column change. This could possibly be implemented,
+ * but it's not.
+ */
+ if (flags & CHKATYPE_IS_VIRTUAL)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("virtual generated column \"%s\" cannot have a domain type", attname));
+
+ /*
* If it's a domain, recurse to check its base type.
*/
CheckAttributeType(attname, getBaseType(atttypid), attcollation,
@@ -2553,6 +2564,11 @@ 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
@@ -2868,6 +2884,11 @@ 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/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 41ffd494c81..d6f94db5d99 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -546,7 +546,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
* pub_collist_validate
* Process and validate the 'columns' list and ensure the columns are all
* valid to use for a publication. Checks for and raises an ERROR for
- * any unknown columns, system columns, or duplicate columns.
+ * any unknown columns, system columns, duplicate columns, or virtual
+ * generated columns.
*
* Looks up each column's attnum and returns a 0-based Bitmapset of the
* corresponding attnums.
@@ -556,6 +557,7 @@ pub_collist_validate(Relation targetrel, List *columns)
{
Bitmapset *set = NULL;
ListCell *lc;
+ TupleDesc tupdesc = RelationGetDescr(targetrel);
foreach(lc, columns)
{
@@ -574,6 +576,12 @@ pub_collist_validate(Relation targetrel, List *columns)
errmsg("cannot use system column \"%s\" in publication column list",
colname));
+ if (TupleDescAttr(tupdesc, attnum - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+ errmsg("cannot use virtual generated column \"%s\" in publication column list",
+ colname));
+
if (bms_is_member(attnum, set))
ereport(ERROR,
errcode(ERRCODE_DUPLICATE_OBJECT),
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index f8da32e9aef..e5ab207d2ec 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -1039,6 +1039,10 @@ examine_attribute(Relation onerel, int attnum, Node *index_expr)
if (attr->attisdropped)
return NULL;
+ /* Don't analyze virtual generated columns */
+ if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ return NULL;
+
/*
* Get attstattarget value. Set to -1 if null. (Analyze functions expect
* -1 to mean use default_statistics_target; see for example
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 5b1753d4681..f8d3ea820e1 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1110,6 +1110,9 @@ DefineIndex(Oid tableId,
/*
* We disallow indexes on system columns. They would not necessarily get
* updated correctly, and they don't seem useful anyway.
+ *
+ * Also disallow virtual generated columns in indexes (use expression
+ * index instead).
*/
for (int i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
{
@@ -1119,14 +1122,24 @@ DefineIndex(Oid tableId,
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("index creation on system columns is not supported")));
+
+
+ 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")));
}
/*
- * Also check for system columns used in expressions or predicates.
+ * Also check for system and generated columns used in expressions or
+ * predicates.
*/
if (indexInfo->ii_Expressions || indexInfo->ii_Predicate)
{
Bitmapset *indexattrs = NULL;
+ int j;
pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs);
pull_varattnos((Node *) indexInfo->ii_Predicate, 1, &indexattrs);
@@ -1139,6 +1152,24 @@ DefineIndex(Oid tableId,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("index creation on system columns is not supported")));
}
+
+ /*
+ * XXX Virtual generated columns in index expressions or predicates
+ * could be supported, but it needs support in
+ * RelationGetIndexExpressions() and RelationGetIndexPredicate().
+ */
+ j = -1;
+ while ((j = bms_next_member(indexattrs, j)) >= 0)
+ {
+ AttrNumber attno = j + FirstLowInvalidHeapAttributeNumber;
+
+ 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")));
+ }
}
/* Is index safe for others to ignore? See set_indexsafe_procflags() */
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 951ffabb656..150a768d16f 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -38,6 +38,7 @@
#include "parser/parse_clause.h"
#include "parser/parse_collate.h"
#include "parser/parse_relation.h"
+#include "rewrite/rewriteHandler.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
#include "utils/builtins.h"
@@ -404,6 +405,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
relation->rd_att->constr->has_generated_stored)
*invalid_gen_col = true;
+ /*
+ * Virtual generated columns are currently not supported for logical
+ * replication at all.
+ */
+ if (relation->rd_att->constr &&
+ relation->rd_att->constr->has_generated_virtual)
+ *invalid_gen_col = true;
+
if (*invalid_gen_col && *invalid_column_list)
return true;
}
@@ -430,7 +439,17 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
* The publish_generated_columns option must be set to stored if
* the REPLICA IDENTITY contains any stored generated column.
*/
- if (pubgencols_type != PUBLISH_GENCOLS_STORED && att->attgenerated)
+ if (att->attgenerated == ATTRIBUTE_GENERATED_STORED && pubgencols_type != PUBLISH_GENCOLS_STORED)
+ {
+ *invalid_gen_col = true;
+ break;
+ }
+
+ /*
+ * The equivalent setting for virtual generated columns does not
+ * exist yet.
+ */
+ if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
{
*invalid_gen_col = true;
break;
@@ -689,6 +708,8 @@ TransformPubWhereClauses(List *tables, const char *queryString,
/* Fix up collation information */
assign_expr_collations(pstate, whereclause);
+ whereclause = expand_generated_columns_in_expr(whereclause, pri->relation, 1);
+
/*
* We allow only simple expressions in row filters. See
* check_simple_rowfilter_expr_walker.
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index a817821bf6d..e24d540cd45 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -246,6 +246,12 @@ CreateStatistics(CreateStatsStmt *stmt)
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("statistics creation on system columns is not supported")));
+ /* Disallow use of virtual generated columns in extended stats */
+ if (attForm->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("statistics creation on virtual generated columns is not supported")));
+
/* Disallow data types without a less-than operator */
type = lookup_type_cache(attForm->atttypid, TYPECACHE_LT_OPR);
if (type->lt_opr == InvalidOid)
@@ -269,6 +275,12 @@ CreateStatistics(CreateStatsStmt *stmt)
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("statistics creation on system columns is not supported")));
+ /* Disallow use of virtual generated columns in extended stats */
+ if (get_attgenerated(relid, var->varattno) == ATTRIBUTE_GENERATED_VIRTUAL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("statistics creation on virtual generated columns is not supported")));
+
/* Disallow data types without a less-than operator */
type = lookup_type_cache(var->vartype, TYPECACHE_LT_OPR);
if (type->lt_opr == InvalidOid)
@@ -290,7 +302,6 @@ CreateStatistics(CreateStatsStmt *stmt)
Assert(expr != NULL);
- /* Disallow expressions referencing system attributes. */
pull_varattnos(expr, 1, &attnums);
k = -1;
@@ -298,10 +309,17 @@ CreateStatistics(CreateStatsStmt *stmt)
{
AttrNumber attnum = k + FirstLowInvalidHeapAttributeNumber;
+ /* Disallow expressions referencing system attributes. */
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("statistics creation on system columns is not supported")));
+
+ /* Disallow use of virtual generated columns in extended stats */
+ if (get_attgenerated(relid, attnum) == ATTRIBUTE_GENERATED_VIRTUAL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("statistics creation on virtual generated columns is not supported")));
}
/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 18f64db6e39..94660e4bd45 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3039,6 +3039,15 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
errhint("A child table column cannot be generated unless its parent column is.")));
}
+ if (coldef->generated && restdef->generated && coldef->generated != restdef->generated)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+ errmsg("column \"%s\" inherits from generated column of different kind",
+ restdef->colname),
+ errdetail("Parent column is %s, child column is %s.",
+ coldef->generated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL",
+ restdef->generated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL")));
+
/*
* Override the parent's default value for this column
* (coldef->cooked_default) with the partition's local
@@ -3324,6 +3333,15 @@ MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const
errhint("A child table column cannot be generated unless its parent column is.")));
}
+ if (inhdef->generated && newdef->generated && newdef->generated != inhdef->generated)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+ errmsg("column \"%s\" inherits from generated column of different kind",
+ inhdef->colname),
+ errdetail("Parent column is %s, child column is %s.",
+ inhdef->generated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL",
+ newdef->generated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL")));
+
/*
* If new def has a default, override previous default
*/
@@ -6130,7 +6148,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
{
case CONSTR_CHECK:
needscan = true;
- con->qualstate = ExecPrepareExpr((Expr *) con->qual, estate);
+ con->qualstate = ExecPrepareExpr((Expr *) expand_generated_columns_in_expr(con->qual, newrel ? newrel : oldrel, 1), estate);
break;
case CONSTR_FOREIGN:
/* Nothing to do here */
@@ -7308,7 +7326,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
* DEFAULT value outside of the heap. This may be disabled inside
* AddRelationNewConstraints if the optimization cannot be applied.
*/
- rawEnt->missingMode = (!colDef->generated);
+ rawEnt->missingMode = (colDef->generated != ATTRIBUTE_GENERATED_STORED);
rawEnt->generated = colDef->generated;
@@ -7785,6 +7803,14 @@ 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))
@@ -8411,6 +8437,8 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
HeapTuple tuple;
Form_pg_attribute attTup;
AttrNumber attnum;
+ char attgenerated;
+ bool rewrite;
Oid attrdefoid;
ObjectAddress address;
Expr *defval;
@@ -8425,36 +8453,70 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
colName, RelationGetRelationName(rel))));
attTup = (Form_pg_attribute) GETSTRUCT(tuple);
- attnum = attTup->attnum;
+ attnum = attTup->attnum;
if (attnum <= 0)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot alter system column \"%s\"",
colName)));
- if (attTup->attgenerated != ATTRIBUTE_GENERATED_STORED)
+ attgenerated = attTup->attgenerated;
+ if (!attgenerated)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("column \"%s\" of relation \"%s\" is not a generated column",
colName, RelationGetRelationName(rel))));
- ReleaseSysCache(tuple);
/*
- * Clear all the missing values if we're rewriting the table, since this
- * renders them pointless.
+ * TODO: This could be done, just need to recheck any constraints
+ * afterwards.
*/
- RelationClearMissing(rel);
-
- /* make sure we don't conflict with later attribute modifications */
- CommandCounterIncrement();
+ if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
+ rel->rd_att->constr && rel->rd_att->constr->num_check > 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables with check constraints"),
+ errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
+ colName, RelationGetRelationName(rel))));
/*
- * Find everything that depends on the column (constraints, indexes, etc),
- * and record enough information to let us recreate the objects after
- * rewrite.
+ * 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
+ * filter. XXX We could try to have a more precise check to catch only
+ * publications with row filters, or even re-verify the row filter
+ * expressions.
*/
- RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName);
+ if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
+ GetRelationPublications(RelationGetRelid(rel)) != NIL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables that are part of a publication"),
+ errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
+ colName, RelationGetRelationName(rel))));
+
+ rewrite = (attgenerated == ATTRIBUTE_GENERATED_STORED);
+
+ ReleaseSysCache(tuple);
+
+ if (rewrite)
+ {
+ /*
+ * Clear all the missing values if we're rewriting the table, since
+ * this renders them pointless.
+ */
+ RelationClearMissing(rel);
+
+ /* make sure we don't conflict with later attribute modifications */
+ CommandCounterIncrement();
+
+ /*
+ * Find everything that depends on the column (constraints, indexes,
+ * etc), and record enough information to let us recreate the objects
+ * after rewrite.
+ */
+ RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName);
+ }
/*
* Drop the dependency records of the GENERATED expression, in particular
@@ -8483,7 +8545,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
rawEnt->attnum = attnum;
rawEnt->raw_default = newExpr;
rawEnt->missingMode = false;
- rawEnt->generated = ATTRIBUTE_GENERATED_STORED;
+ rawEnt->generated = attgenerated;
/* Store the generated expression */
AddRelationNewConstraints(rel, list_make1(rawEnt), NIL,
@@ -8492,16 +8554,19 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
/* Make above new expression visible */
CommandCounterIncrement();
- /* Prepare for table rewrite */
- defval = (Expr *) build_column_default(rel, attnum);
+ if (rewrite)
+ {
+ /* Prepare for table rewrite */
+ defval = (Expr *) build_column_default(rel, attnum);
- newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue));
- newval->attnum = attnum;
- newval->expr = expression_planner(defval);
- newval->is_generated = true;
+ newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue));
+ newval->attnum = attnum;
+ newval->expr = expression_planner(defval);
+ newval->is_generated = true;
- tab->newvals = lappend(tab->newvals, newval);
- tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+ tab->newvals = lappend(tab->newvals, newval);
+ tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+ }
/* Drop any pg_statistic entry for the column */
RemoveStatistics(RelationGetRelid(rel), attnum);
@@ -8590,17 +8655,30 @@ ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMOD
errmsg("cannot alter system column \"%s\"",
colName)));
- if (attTup->attgenerated != ATTRIBUTE_GENERATED_STORED)
+ /*
+ * TODO: This could be done, but it would need a table rewrite to
+ * materialize the generated values. Note that for the time being, we
+ * still error with missing_ok, so that we don't silently leave the column
+ * as generated.
+ */
+ if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("ALTER TABLE / DROP EXPRESSION is not supported for virtual generated columns"),
+ errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
+ colName, RelationGetRelationName(rel))));
+
+ if (!attTup->attgenerated)
{
if (!missing_ok)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("column \"%s\" of relation \"%s\" is not a stored generated column",
+ errmsg("column \"%s\" of relation \"%s\" is not a generated column",
colName, RelationGetRelationName(rel))));
else
{
ereport(NOTICE,
- (errmsg("column \"%s\" of relation \"%s\" is not a stored generated column, skipping",
+ (errmsg("column \"%s\" of relation \"%s\" is not a generated column, skipping",
colName, RelationGetRelationName(rel))));
heap_freetuple(tuple);
table_close(attrelation, RowExclusiveLock);
@@ -8743,6 +8821,16 @@ ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa
errmsg("cannot alter system column \"%s\"",
colName)));
+ /*
+ * Prevent this as long as the ANALYZE code skips virtual generated
+ * columns.
+ */
+ if (attrtuple->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter statistics on virtual generated column \"%s\"",
+ colName)));
+
if (rel->rd_rel->relkind == RELKIND_INDEX ||
rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
{
@@ -9925,6 +10013,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
errmsg("invalid %s action for foreign key constraint containing generated column",
"ON DELETE")));
}
+
+ /*
+ * FKs on virtual columns are not supported. This would require
+ * various additional support in ri_triggers.c, including special
+ * handling in ri_NullCheck(), ri_KeysEqual(),
+ * RI_FKey_fk_upd_check_required() (since all virtual columns appear
+ * as NULL there). Also not really practical as long as you can't
+ * index virtual columns.
+ */
+ if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("foreign key constraints on virtual generated columns are not supported")));
}
/*
@@ -12289,7 +12390,7 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
val = SysCacheGetAttrNotNull(CONSTROID, contuple,
Anum_pg_constraint_conbin);
conbin = TextDatumGetCString(val);
- newcon->qual = (Node *) stringToNode(conbin);
+ newcon->qual = expand_generated_columns_in_expr(stringToNode(conbin), rel, 1);
/* Find or create work queue entry for this table */
tab = ATGetQueueEntry(wqueue, rel);
@@ -13467,8 +13568,12 @@ ATPrepAlterColumnType(List **wqueue,
list_make1_oid(rel->rd_rel->reltype),
0);
- if (tab->relkind == RELKIND_RELATION ||
- tab->relkind == RELKIND_PARTITIONED_TABLE)
+ if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ {
+ /* do nothing */
+ }
+ else if (tab->relkind == RELKIND_RELATION ||
+ tab->relkind == RELKIND_PARTITIONED_TABLE)
{
/*
* Set up an expression to transform the old data value to the new
@@ -13541,11 +13646,12 @@ ATPrepAlterColumnType(List **wqueue,
errmsg("\"%s\" is not a table",
RelationGetRelationName(rel))));
- if (!RELKIND_HAS_STORAGE(tab->relkind))
+ if (!RELKIND_HAS_STORAGE(tab->relkind) || attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
{
/*
- * For relations without storage, do this check now. Regular tables
- * will check it later when the table is being rewritten.
+ * For relations or columns without storage, do this check now.
+ * Regular tables will check it later when the table is being
+ * rewritten.
*/
find_composite_type_dependencies(rel->rd_rel->reltype, rel, NULL);
}
@@ -16534,6 +16640,14 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("column \"%s\" in child table must not be a generated column", parent_attname)));
+ if (parent_att->attgenerated && child_att->attgenerated && child_att->attgenerated != parent_att->attgenerated)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("column \"%s\" inherits from generated column of different kind", parent_attname),
+ errdetail("Parent column is %s, child column is %s.",
+ parent_att->attgenerated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL",
+ child_att->attgenerated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL")));
+
/*
* Regular inheritance children are independent enough not to
* inherit identity columns. But partitions are integral part of
@@ -18781,8 +18895,11 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
parser_errposition(pstate, pelem->location)));
/*
- * Generated columns cannot work: They are computed after BEFORE
- * triggers, but partition routing is done before all triggers.
+ * Stored generated columns cannot work: They are computed after
+ * BEFORE triggers, but partition routing is done before all
+ * triggers. Maybe virtual generated columns could be made to
+ * work, but then they would need to be handled as an expression
+ * below.
*/
if (attform->attgenerated)
ereport(ERROR,
@@ -18864,9 +18981,12 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
}
/*
- * Generated columns cannot work: They are computed after
- * BEFORE triggers, but partition routing is done before all
- * triggers.
+ * Stored generated columns cannot work: They are computed
+ * after BEFORE triggers, but partition routing is done before
+ * all triggers. Virtual generated columns could probably
+ * work, but it would require more work elsewhere (for example
+ * SET EXPRESSION would need to check whether the column is
+ * used in partition keys). Seems safer to prohibit for now.
*/
i = -1;
while ((i = bms_next_member(expr_attrs, i)) >= 0)
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 7a5ffe32f60..97c087929f3 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -43,6 +43,7 @@
#include "parser/parse_relation.h"
#include "partitioning/partdesc.h"
#include "pgstat.h"
+#include "rewrite/rewriteHandler.h"
#include "rewrite/rewriteManip.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
@@ -101,6 +102,7 @@ static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
bool is_crosspart_update);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
+static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple tuple);
/*
@@ -641,7 +643,8 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
if (TRIGGER_FOR_BEFORE(tgtype) &&
var->varattno == 0 &&
RelationGetDescr(rel)->constr &&
- RelationGetDescr(rel)->constr->has_generated_stored)
+ (RelationGetDescr(rel)->constr->has_generated_stored ||
+ RelationGetDescr(rel)->constr->has_generated_virtual))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
@@ -2504,6 +2507,8 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
}
else if (newtuple != oldtuple)
{
+ newtuple = check_modified_virtual_generated(RelationGetDescr(relinfo->ri_RelationDesc), newtuple);
+
ExecForceStoreHeapTuple(newtuple, slot, false);
/*
@@ -3061,6 +3066,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
}
else if (newtuple != oldtuple)
{
+ newtuple = check_modified_virtual_generated(RelationGetDescr(relinfo->ri_RelationDesc), newtuple);
+
ExecForceStoreHeapTuple(newtuple, newslot, false);
/*
@@ -3491,6 +3498,8 @@ TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
tgqual = stringToNode(trigger->tgqual);
+ tgqual = expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc, PRS2_OLD_VARNO);
+ tgqual = expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc, PRS2_NEW_VARNO);
/* Change references to OLD and NEW to INNER_VAR and OUTER_VAR */
ChangeVarNodes(tgqual, PRS2_OLD_VARNO, INNER_VAR, 0);
ChangeVarNodes(tgqual, PRS2_NEW_VARNO, OUTER_VAR, 0);
@@ -6621,3 +6630,37 @@ pg_trigger_depth(PG_FUNCTION_ARGS)
{
PG_RETURN_INT32(MyTriggerDepth);
}
+
+/*
+ * Check whether a trigger modified a virtual generated column and replace the
+ * value with null if so.
+ *
+ * We need to check this so that we don't end up storing a non-null value in a
+ * virtual generated column.
+ *
+ * We don't need to check for stored generated columns, since those will be
+ * overwritten later anyway.
+ */
+static HeapTuple
+check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple tuple)
+{
+ if (!(tupdesc->constr && tupdesc->constr->has_generated_virtual))
+ return tuple;
+
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ {
+ if (!heap_attisnull(tuple, i + 1, tupdesc))
+ {
+ int replCol = i + 1;
+ Datum replValue = 0;
+ bool replIsnull = true;
+
+ tuple = heap_modify_tuple_by_cols(tuple, tupdesc, 1, &replCol, &replValue, &replIsnull);
+ }
+ }
+ }
+
+ return tuple;
+}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 09f6a5f14c1..1c3477b03c9 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -2324,6 +2324,10 @@ CheckVarSlotCompatibility(TupleTableSlot *slot, int attnum, Oid vartype)
attr = TupleDescAttr(slot_tupdesc, attnum - 1);
+ /* Internal error: somebody forgot to expand it. */
+ if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ elog(ERROR, "unexpected virtual generated column reference");
+
if (attr->attisdropped)
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 74ef35cd250..39d80ccfbad 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1781,6 +1781,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
continue;
checkconstr = stringToNode(check[i].ccbin);
+ checkconstr = (Expr *) expand_generated_columns_in_expr((Node *) checkconstr, rel, 1);
resultRelInfo->ri_ConstraintExprs[i] =
ExecPrepareExpr(checkconstr, estate);
}
@@ -2328,7 +2329,9 @@ ExecBuildSlotValueDescription(Oid reloid,
if (table_perm || column_perm)
{
- if (slot->tts_isnull[i])
+ if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ val = "virtual";
+ else if (slot->tts_isnull[i])
val = "null";
else
{
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index c9c756f8568..448fc09aaac 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -1393,8 +1393,8 @@ Bitmapset *
ExecGetExtraUpdatedCols(ResultRelInfo *relinfo, EState *estate)
{
/* Compute the info if we didn't already */
- if (relinfo->ri_GeneratedExprsU == NULL)
- ExecInitStoredGenerated(relinfo, estate, CMD_UPDATE);
+ if (!relinfo->ri_extraUpdatedCols_valid)
+ ExecInitGenerated(relinfo, estate, CMD_UPDATE);
return relinfo->ri_extraUpdatedCols;
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 349ed2d6d2c..a15e7863b0d 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -391,11 +391,14 @@ ExecCheckTIDVisible(EState *estate,
}
/*
- * Initialize to compute stored generated columns for a tuple
+ * Initialize generated columns handling for a tuple
+ *
+ * This fills the resultRelInfo's ri_GeneratedExprsI/ri_NumGeneratedNeededI or
+ * ri_GeneratedExprsU/ri_NumGeneratedNeededU fields, depending on cmdtype.
+ * This is used only for stored generated columns.
*
- * This fills the resultRelInfo's ri_GeneratedExprsI/ri_NumGeneratedNeededI
- * or ri_GeneratedExprsU/ri_NumGeneratedNeededU fields, depending on cmdtype.
* If cmdType == CMD_UPDATE, the ri_extraUpdatedCols field is filled too.
+ * This is used by both stored and virtual generated columns.
*
* Note: usually, a given query would need only one of ri_GeneratedExprsI and
* ri_GeneratedExprsU per result rel; but MERGE can need both, and so can
@@ -403,9 +406,9 @@ ExecCheckTIDVisible(EState *estate,
* UPDATE and INSERT actions.
*/
void
-ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
- EState *estate,
- CmdType cmdtype)
+ExecInitGenerated(ResultRelInfo *resultRelInfo,
+ EState *estate,
+ CmdType cmdtype)
{
Relation rel = resultRelInfo->ri_RelationDesc;
TupleDesc tupdesc = RelationGetDescr(rel);
@@ -416,7 +419,7 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
MemoryContext oldContext;
/* Nothing to do if no generated columns */
- if (!(tupdesc->constr && tupdesc->constr->has_generated_stored))
+ if (!(tupdesc->constr && (tupdesc->constr->has_generated_stored || tupdesc->constr->has_generated_virtual)))
return;
/*
@@ -442,7 +445,9 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
for (int i = 0; i < natts; i++)
{
- if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_STORED)
+ char attgenerated = TupleDescAttr(tupdesc, i)->attgenerated;
+
+ if (attgenerated)
{
Expr *expr;
@@ -467,8 +472,11 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
}
/* No luck, so prepare the expression for execution */
- ri_GeneratedExprs[i] = ExecPrepareExpr(expr, estate);
- ri_NumGeneratedNeeded++;
+ if (attgenerated == ATTRIBUTE_GENERATED_STORED)
+ {
+ ri_GeneratedExprs[i] = ExecPrepareExpr(expr, estate);
+ ri_NumGeneratedNeeded++;
+ }
/* If UPDATE, mark column in resultRelInfo->ri_extraUpdatedCols */
if (cmdtype == CMD_UPDATE)
@@ -478,6 +486,13 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
}
}
+ if (ri_NumGeneratedNeeded == 0)
+ {
+ /* didn't need it after all */
+ pfree(ri_GeneratedExprs);
+ ri_GeneratedExprs = NULL;
+ }
+
/* Save in appropriate set of fields */
if (cmdtype == CMD_UPDATE)
{
@@ -486,6 +501,8 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_GeneratedExprsU = ri_GeneratedExprs;
resultRelInfo->ri_NumGeneratedNeededU = ri_NumGeneratedNeeded;
+
+ resultRelInfo->ri_extraUpdatedCols_valid = true;
}
else
{
@@ -526,7 +543,7 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
if (cmdtype == CMD_UPDATE)
{
if (resultRelInfo->ri_GeneratedExprsU == NULL)
- ExecInitStoredGenerated(resultRelInfo, estate, cmdtype);
+ ExecInitGenerated(resultRelInfo, estate, cmdtype);
if (resultRelInfo->ri_NumGeneratedNeededU == 0)
return;
ri_GeneratedExprs = resultRelInfo->ri_GeneratedExprsU;
@@ -534,7 +551,7 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
else
{
if (resultRelInfo->ri_GeneratedExprsI == NULL)
- ExecInitStoredGenerated(resultRelInfo, estate, cmdtype);
+ ExecInitGenerated(resultRelInfo, estate, cmdtype);
/* Early exit is impossible given the prior Assert */
Assert(resultRelInfo->ri_NumGeneratedNeededI > 0);
ri_GeneratedExprs = resultRelInfo->ri_GeneratedExprsI;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d7f9c00c409..d3887628d46 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -636,7 +636,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <str> opt_existing_window_name
%type <boolean> opt_if_not_exists
%type <boolean> opt_unique_null_treatment
-%type <ival> generated_when override_kind
+%type <ival> generated_when override_kind opt_virtual_or_stored
%type <partspec> PartitionSpec OptPartitionSpec
%type <partelem> part_elem
%type <list> part_params
@@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
UNLISTEN UNLOGGED UNTIL UPDATE USER USING
VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
- VERBOSE VERSION_P VIEW VIEWS VOLATILE
+ VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE
WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
@@ -3995,7 +3995,7 @@ ColConstraintElem:
n->location = @1;
$$ = (Node *) n;
}
- | GENERATED generated_when AS '(' a_expr ')' STORED
+ | GENERATED generated_when AS '(' a_expr ')' opt_virtual_or_stored
{
Constraint *n = makeNode(Constraint);
@@ -4003,6 +4003,7 @@ ColConstraintElem:
n->generated_when = $2;
n->raw_expr = $5;
n->cooked_expr = NULL;
+ n->generated_kind = $7;
n->location = @1;
/*
@@ -4050,6 +4051,12 @@ generated_when:
| BY DEFAULT { $$ = ATTRIBUTE_IDENTITY_BY_DEFAULT; }
;
+opt_virtual_or_stored:
+ STORED { $$ = ATTRIBUTE_GENERATED_STORED; }
+ | VIRTUAL { $$ = ATTRIBUTE_GENERATED_VIRTUAL; }
+ | /*EMPTY*/ { $$ = ATTRIBUTE_GENERATED_VIRTUAL; }
+ ;
+
/*
* ConstraintAttr represents constraint attributes, which we parse as if
* they were independent constraint clauses, in order to avoid shift/reduce
@@ -17990,6 +17997,7 @@ unreserved_keyword:
| VERSION_P
| VIEW
| VIEWS
+ | VIRTUAL
| VOLATILE
| WHITESPACE_P
| WITHIN
@@ -18645,6 +18653,7 @@ bare_label_keyword:
| VERSION_P
| VIEW
| VIEWS
+ | VIRTUAL
| VOLATILE
| WHEN
| WHITESPACE_P
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 101fba34b18..04ecf64b1fc 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -712,7 +712,11 @@ scanNSItemForColumn(ParseState *pstate, ParseNamespaceItem *nsitem,
colname),
parser_errposition(pstate, location)));
- /* In generated column, no system column is allowed except tableOid */
+ /*
+ * In generated column, no system column is allowed except tableOid.
+ * (Required for stored generated, but we also do it for virtual generated
+ * for now for consistency.)
+ */
if (pstate->p_expr_kind == EXPR_KIND_GENERATED_COLUMN &&
attnum < InvalidAttrNumber && attnum != TableOidAttributeNumber)
ereport(ERROR,
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index ca028d2a66d..eb7716cd84c 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -889,7 +889,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
column->colname, cxt->relation->relname),
parser_errposition(cxt->pstate,
constraint->location)));
- column->generated = ATTRIBUTE_GENERATED_STORED;
+ column->generated = constraint->generated_kind;
column->raw_default = constraint->raw_expr;
Assert(constraint->cooked_expr == NULL);
saw_generated = true;
@@ -988,6 +988,20 @@ 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/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 2f89996a757..7d464f656aa 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -27,6 +27,7 @@
#include "replication/logicalproto.h"
#include "replication/origin.h"
#include "replication/pgoutput.h"
+#include "rewrite/rewriteHandler.h"
#include "utils/builtins.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
@@ -1010,7 +1011,7 @@ pgoutput_row_filter_init(PGOutputData *data, List *publications,
continue;
foreach(lc, rfnodes[idx])
- filters = lappend(filters, stringToNode((char *) lfirst(lc)));
+ filters = lappend(filters, expand_generated_columns_in_expr(stringToNode((char *) lfirst(lc)), relation, 1));
/* combine the row filter and cache the ExprState */
rfnode = make_orclause(filters);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 847edcfa90e..e996bdc0d21 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -96,6 +96,8 @@ static List *matchLocks(CmdType event, Relation relation,
int varno, Query *parsetree, bool *hasUpdate);
static Query *fireRIRrules(Query *parsetree, List *activeRIRs);
static Bitmapset *adjust_view_column_set(Bitmapset *cols, List *targetlist);
+static Node *expand_generated_columns_internal(Node *node, Relation rel, int rt_index,
+ RangeTblEntry *rte, int result_relation);
/*
@@ -986,7 +988,8 @@ rewriteTargetListIU(List *targetList,
if (att_tup->attgenerated)
{
/*
- * stored generated column will be fixed in executor
+ * virtual generated column stores a null value; stored generated
+ * column will be fixed in executor
*/
new_tle = NULL;
}
@@ -2187,6 +2190,10 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
* requires special recursion detection if the new quals have sublink
* subqueries, and if we did it in the loop above query_tree_walker would
* then recurse into those quals a second time.
+ *
+ * Finally, we expand any virtual generated columns. We do this after
+ * each table's RLS policies are applied because the RLS policies might
+ * also refer to the table's virtual generated columns.
*/
rt_index = 0;
foreach(lc, parsetree->rtable)
@@ -2200,10 +2207,11 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
++rt_index;
- /* Only normal relations can have RLS policies */
- if (rte->rtekind != RTE_RELATION ||
- (rte->relkind != RELKIND_RELATION &&
- rte->relkind != RELKIND_PARTITIONED_TABLE))
+ /*
+ * Only normal relations can have RLS policies or virtual generated
+ * columns.
+ */
+ if (rte->rtekind != RTE_RELATION)
continue;
rel = table_open(rte->relid, NoLock);
@@ -2292,6 +2300,16 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
if (hasSubLinks)
parsetree->hasSubLinks = true;
+ /*
+ * Expand any references to virtual generated columns of this table.
+ * Note that subqueries in virtual generated column expressions are
+ * not currently supported, so this cannot add any more sublinks.
+ */
+ parsetree = (Query *)
+ expand_generated_columns_internal((Node *) parsetree,
+ rel, rt_index, rte,
+ parsetree->resultRelation);
+
table_close(rel, NoLock);
}
@@ -4407,6 +4425,111 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
/*
+ * Expand virtual generated columns
+ *
+ * If the table contains virtual generated columns, build a target list
+ * containing the expanded expressions and use ReplaceVarsFromTargetList() to
+ * do the replacements.
+ *
+ * Vars matching rt_index at the current query level are replaced by the
+ * virtual generated column expressions from rel, if there are any.
+ *
+ * The caller must also provide rte, the RTE describing the target relation,
+ * in order to handle any whole-row Vars referencing the target, and
+ * result_relation, the index of the result relation, if this is part of an
+ * INSERT/UPDATE/DELETE/MERGE query.
+ */
+static Node *
+expand_generated_columns_internal(Node *node, Relation rel, int rt_index,
+ RangeTblEntry *rte, int result_relation)
+{
+ TupleDesc tupdesc;
+
+ tupdesc = RelationGetDescr(rel);
+ if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
+ {
+ List *tlist = NIL;
+
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+
+ if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ {
+ Node *defexpr;
+ int attnum = i + 1;
+ Oid attcollid;
+ TargetEntry *te;
+
+ defexpr = build_column_default(rel, attnum);
+ if (defexpr == NULL)
+ elog(ERROR, "no generation expression found for column number %d of table \"%s\"",
+ attnum, RelationGetRelationName(rel));
+
+ /*
+ * If the column definition has a collation and it is
+ * different from the collation of the generation expression,
+ * put a COLLATE clause around the expression.
+ */
+ attcollid = attr->attcollation;
+ if (attcollid && attcollid != exprCollation(defexpr))
+ {
+ CollateExpr *ce = makeNode(CollateExpr);
+
+ ce->arg = (Expr *) defexpr;
+ ce->collOid = attcollid;
+ ce->location = -1;
+
+ defexpr = (Node *) ce;
+ }
+
+ ChangeVarNodes(defexpr, 1, rt_index, 0);
+
+ te = makeTargetEntry((Expr *) defexpr, attnum, 0, false);
+ tlist = lappend(tlist, te);
+ }
+ }
+
+ Assert(list_length(tlist) > 0);
+
+ node = ReplaceVarsFromTargetList(node, rt_index, 0, rte, tlist,
+ result_relation,
+ REPLACEVARS_CHANGE_VARNO, rt_index,
+ NULL);
+ }
+
+ return node;
+}
+
+/*
+ * Expand virtual generated columns in an expression
+ *
+ * This is for expressions that are not part of a query, such as default
+ * expressions or index predicates. The rt_index is usually 1.
+ */
+Node *
+expand_generated_columns_in_expr(Node *node, Relation rel, int rt_index)
+{
+ TupleDesc tupdesc = RelationGetDescr(rel);
+
+ if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
+ {
+ RangeTblEntry *rte;
+
+ rte = makeNode(RangeTblEntry);
+ /* eref needs to be set, but the actual name doesn't matter */
+ rte->eref = makeAlias(RelationGetRelationName(rel), NIL);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+
+ node = expand_generated_columns_internal(node, rel, rt_index, rte, 0);
+ }
+
+ return node;
+}
+
+
+/*
* QueryRewrite -
* Primary entry point to the query rewriter.
* Rewrite one query via query rewrite system, possibly returning 0
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 43219a9629c..398114373e9 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -592,6 +592,8 @@ RelationBuildTupleDesc(Relation relation)
constr->has_not_null = true;
if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
constr->has_generated_stored = true;
+ if (attp->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ constr->has_generated_virtual = true;
if (attp->atthasdef)
ndef++;
@@ -674,6 +676,7 @@ RelationBuildTupleDesc(Relation relation)
*/
if (constr->has_not_null ||
constr->has_generated_stored ||
+ constr->has_generated_virtual ||
ndef > 0 ||
attrmiss ||
relation->rd_rel->relchecks > 0)