@@ -62,6 +62,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
NULL |
CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
DEFAULT <replaceable>default_expr</replaceable> |
GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED |
GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( <replaceable>sequence_options</replaceable> ) ] |
UNIQUE <replaceable class="parameter">index_parameters</replaceable> |
PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
@@ -83,7 +84,7 @@ class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable cl

<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>

{ INCLUDING | EXCLUDING } { COMMENTS | CONSTRAINTS | DEFAULTS | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
{ INCLUDING | EXCLUDING } { COMMENTS | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }

<phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>

@@ -627,6 +628,16 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>

<varlistentry>
<term><literal>INCLUDING GENERATED</literal></term>
<listitem>
<para>
Any generation expressions of copied column definitions will be
copied. By default, new columns will be regular base columns.
</para>
</listitem>
</varlistentry>

<varlistentry>
<term><literal>INCLUDING IDENTITY</literal></term>
<listitem>
@@ -797,6 +808,28 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>

<varlistentry>
<term><literal>GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED</literal><indexterm><primary>generated column</primary></indexterm></term>
<listitem>
<para>
This clause creates the column as a <firstterm>generated
column</firstterm>. The column cannot be written to, and when read it
will be computed from the specified expression.
</para>

<para>
The keyword <literal>STORED</literal> is required to signify that the
column will be computed on write and will be stored on disk. default.
</para>

<para>
The generation expression can refer to other columns in the table, but
not other generated columns. Any functions and operators used must be
immutable. References to other tables are not allowed.
</para>
</listitem>
</varlistentry>

<varlistentry>
<term><literal>GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( <replaceable>sequence_options</replaceable> ) ]</literal></term>
<listitem>
@@ -2028,6 +2061,16 @@ CREATE TABLE cities_partdef
</para>
</refsect2>

<refsect2>
<title>Generated Columns</title>

<para>
The option <literal>STORED</literal> is not standard but is also used by
other SQL implementations. The SQL standard does not specify the storage
of generated columns.
</para>
</refsect2>

<refsect2>
<title><literal>LIKE</literal> Clause</title>

@@ -261,7 +261,9 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="parameter">name</replaceable>
UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</replaceable> ... ]
</synopsis>
The trigger will only fire if at least one of the listed columns
is mentioned as a target of the <command>UPDATE</command> command.
is mentioned as a target of the <command>UPDATE</command> command
or if one of the listed columns is a generated column that depends on a
column that is the target of the <command>UPDATE</command>.
</para>

<para>
@@ -620,15 +620,17 @@ CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' |

<para>
Another approach is to create a separate <type>tsvector</type> column
to hold the output of <function>to_tsvector</function>. This example is a
to hold the output of <function>to_tsvector</function>. To keep this
column automatically up to date with its source data, use a stored
generated column. This example is a
concatenation of <literal>title</literal> and <literal>body</literal>,
using <function>coalesce</function> to ensure that one field will still be
indexed when the other is <literal>NULL</literal>:

<programlisting>
ALTER TABLE pgweb ADD COLUMN textsearchable_index_col tsvector;
UPDATE pgweb SET textsearchable_index_col =
to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,''));
ALTER TABLE pgweb
ADD COLUMN textsearchable_index_col tsvector
GENERATED ALWAYS AS (to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))) STORED;
</programlisting>

Then we create a <acronym>GIN</acronym> index to speed up the search:
@@ -648,14 +650,6 @@ LIMIT 10;
</programlisting>
</para>

<para>
When using a separate column to store the <type>tsvector</type>
representation,
it is necessary to create a trigger to keep the <type>tsvector</type>
column current anytime <literal>title</literal> or <literal>body</literal> changes.
<xref linkend="textsearch-update-triggers"/> explains how to do that.
</para>

<para>
One advantage of the separate-column approach over an expression index
is that it is not necessary to explicitly specify the text search
@@ -1857,6 +1851,14 @@ SELECT ts_rewrite('a &amp; b'::tsquery,
<secondary>for updating a derived tsvector column</secondary>
</indexterm>

<note>
<para>
The method described in this section has been obsoleted by the use of
stored generated columns, as described in <xref
linkend="textsearch-tables-index"/>.
</para>
</note>

<para>
When using a separate column to store the <type>tsvector</type> representation
of your documents, it is necessary to create a trigger to update the
@@ -243,6 +243,24 @@
operation, and so they can return <symbol>NULL</symbol>.
</para>

<para>
Some considerations apply for generated
columns.<indexterm><primary>generated column</primary><secondary>in
triggers</secondary></indexterm> Stored generated columns are computed after
<literal>BEFORE</literal> triggers and before <literal>AFTER</literal>
triggers. Therefore, the generated value can be inspected in
<literal>AFTER</literal> triggers. In <literal>BEFORE</literal> triggers,
the <literal>OLD</literal> row contains the old generated value, as one
would expect, but the <literal>NEW</literal> row does not yet contain the
new generated value and should not be accessed. In the C language
interface, the content of the column is undefined at this point; a
higher-level programming language should prevent access to a stored
generated column in the <literal>NEW</literal> row in a
<literal>BEFORE</literal> trigger. Changes to the value of a generated
column in a <literal>BEFORE</literal> trigger are ignored and will be
overwritten.
</para>

<para>
If more than one trigger is defined for the same event on the same
relation, the triggers will be fired in alphabetical order by
@@ -131,6 +131,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
att->attgenerated = '\0';
}

/* We can copy the tuple type identification, too */
@@ -165,6 +166,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
TupleConstr *cpy = (TupleConstr *) palloc0(sizeof(TupleConstr));

cpy->has_not_null = constr->has_not_null;
cpy->has_generated_stored = constr->has_generated_stored;

if ((cpy->num_defval = constr->num_defval) > 0)
{
@@ -247,6 +249,7 @@ TupleDescCopy(TupleDesc dst, TupleDesc src)
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
att->attgenerated = '\0';
}
dst->constr = NULL;

@@ -300,6 +303,7 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
dstAtt->atthasdef = false;
dstAtt->atthasmissing = false;
dstAtt->attidentity = '\0';
dstAtt->attgenerated = '\0';
}

/*
@@ -456,6 +460,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
return false;
if (attr1->attidentity != attr2->attidentity)
return false;
if (attr1->attgenerated != attr2->attgenerated)
return false;
if (attr1->attisdropped != attr2->attisdropped)
return false;
if (attr1->attislocal != attr2->attislocal)
@@ -476,6 +482,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
return false;
if (constr1->has_not_null != constr2->has_not_null)
return false;
if (constr1->has_generated_stored != constr2->has_generated_stored)
return false;
n = constr1->num_defval;
if (n != (int) constr2->num_defval)
return false;
@@ -638,6 +646,7 @@ TupleDescInitEntry(TupleDesc desc,
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
att->attgenerated = '\0';
att->attisdropped = false;
att->attislocal = true;
att->attinhcount = 0;
@@ -697,6 +706,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
att->attgenerated = '\0';
att->attisdropped = false;
att->attislocal = true;
att->attinhcount = 0;
@@ -853,6 +863,7 @@ BuildDescForRelation(List *schema)
TupleConstr *constr = (TupleConstr *) palloc0(sizeof(TupleConstr));

constr->has_not_null = true;
constr->has_generated_stored = false;
constr->defval = NULL;
constr->missing = NULL;
constr->num_defval = 0;
@@ -71,6 +71,7 @@
#include "parser/parse_collate.h"
#include "parser/parse_expr.h"
#include "parser/parse_relation.h"
#include "parser/parsetree.h"
#include "partitioning/partdesc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
@@ -718,6 +719,7 @@ InsertPgAttributeTuple(Relation pg_attribute_rel,
values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(new_attribute->atthasdef);
values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(new_attribute->atthasmissing);
values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(new_attribute->attidentity);
values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(new_attribute->attgenerated);
values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(new_attribute->attisdropped);
values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(new_attribute->attislocal);
values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(new_attribute->attinhcount);
@@ -1631,6 +1633,9 @@ RemoveAttributeById(Oid relid, AttrNumber attnum)
/* We don't want to keep stats for it anymore */
attStruct->attstattarget = 0;

/* Unset this so no one tries to look up the generation expression */
attStruct->attgenerated = '\0';

/*
* Change the column name to something that isn't likely to conflict
*/
@@ -2130,6 +2135,7 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
Relation attrrel;
HeapTuple atttup;
Form_pg_attribute attStruct;
char attgenerated;
Oid attrdefOid;
ObjectAddress colobject,
defobject;
@@ -2177,6 +2183,7 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
elog(ERROR, "cache lookup failed for attribute %d of relation %u",
attnum, RelationGetRelid(rel));
attStruct = (Form_pg_attribute) GETSTRUCT(atttup);
attgenerated = attStruct->attgenerated;
if (!attStruct->atthasdef)
{
Form_pg_attribute defAttStruct;
@@ -2197,7 +2204,7 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
valuesAtt[Anum_pg_attribute_atthasdef - 1] = true;
replacesAtt[Anum_pg_attribute_atthasdef - 1] = true;

if (add_column_mode)
if (add_column_mode && !attgenerated)
{
expr2 = expression_planner(expr2);
estate = CreateExecutorState();
@@ -2259,7 +2266,26 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
/*
* Record dependencies on objects used in the expression, too.
*/
recordDependencyOnExpr(&defobject, expr, NIL, DEPENDENCY_NORMAL);
if (attgenerated)
{
/*
* Generated column: Dropping anything that the generation expression
* refers to automatically drops the generated column.
*/
recordDependencyOnSingleRelExpr(&colobject, expr, RelationGetRelid(rel),
DEPENDENCY_AUTO,
DEPENDENCY_AUTO, false);
}
else
{
/*
* Normal default: Dropping anything that the default refers to
* requires CASCADE and drops the default only.
*/
recordDependencyOnSingleRelExpr(&defobject, expr, RelationGetRelid(rel),
DEPENDENCY_NORMAL,
DEPENDENCY_NORMAL, false);
}

/*
* Post creation hook for attribute defaults.
@@ -2517,12 +2543,14 @@ AddRelationNewConstraints(Relation rel,

expr = cookDefault(pstate, colDef->raw_default,
atp->atttypid, atp->atttypmod,
NameStr(atp->attname));
NameStr(atp->attname),
atp->attgenerated);

/*
* If the expression is just a NULL constant, we do not bother to make
* an explicit pg_attrdef entry, since the default behavior is
* equivalent.
* equivalent. This applies to column defaults, but not for generation
* expressions.
*
* Note a nonobvious property of this test: if the column is of a
* domain type, what we'll get is not a bare null Const but a
@@ -2531,7 +2559,9 @@ AddRelationNewConstraints(Relation rel,
* override any default that the domain might have.
*/
if (expr == NULL ||
(IsA(expr, Const) &&((Const *) expr)->constisnull))
(!colDef->generated &&
IsA(expr, Const) &&
castNode(Const, expr)->constisnull))
continue;

/* If the DEFAULT is volatile we cannot use a missing value */
@@ -2888,6 +2918,46 @@ SetRelationNumChecks(Relation rel, int numchecks)
table_close(relrel, RowExclusiveLock);
}

/*
* Check for references to generated columns
*/
static bool
check_nested_generated_walker(Node *node, void *context)
{
ParseState *pstate = context;

if (node == NULL)
return false;
else if (IsA(node, Var))
{
Var *var = (Var *) node;
Oid relid;
AttrNumber attnum;

relid = rt_fetch(var->varno, pstate->p_rtable)->relid;
attnum = var->varattno;

if (OidIsValid(relid) && AttributeNumberIsValid(attnum) && get_attgenerated(relid, attnum))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("cannot use generated column \"%s\" in column generation expression",
get_attname(relid, attnum, false)),
errdetail("A generated column cannot reference another generated column."),
parser_errposition(pstate, var->location)));

return false;
}
else
return expression_tree_walker(node, check_nested_generated_walker,
(void *) context);
}

static void
check_nested_generated(ParseState *pstate, Node *node)
{
check_nested_generated_walker(node, pstate);
}

/*
* Take a raw default and convert it to a cooked format ready for
* storage.
@@ -2905,7 +2975,8 @@ cookDefault(ParseState *pstate,
Node *raw_default,
Oid atttypid,
int32 atttypmod,
const char *attname)
const char *attname,
char attgenerated)
{
Node *expr;

@@ -2914,14 +2985,25 @@ cookDefault(ParseState *pstate,
/*
* Transform raw parsetree to executable expression.
*/
expr = transformExpr(pstate, raw_default, EXPR_KIND_COLUMN_DEFAULT);
expr = transformExpr(pstate, raw_default, attgenerated ? EXPR_KIND_GENERATED_COLUMN : EXPR_KIND_COLUMN_DEFAULT);

/*
* transformExpr() should have already rejected column references,
* subqueries, aggregates, window functions, and SRFs, based on the
* EXPR_KIND_ for a default expression.
*/
Assert(!contain_var_clause(expr));
if (attgenerated)
{
check_nested_generated(pstate, expr);

if (contain_mutable_functions(expr))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("generation expression is not immutable")));
}
else
{
/*
* For a default expression, transformExpr() should have rejected
* column references.
*/
Assert(!contain_var_clause(expr));
}

/*
* Coerce the expression to the correct type and typmod, if given. This
@@ -509,7 +509,29 @@ GRANT SELECT ON collation_character_set_applicability TO PUBLIC;
* COLUMN_COLUMN_USAGE view
*/

-- feature not supported
CREATE VIEW column_column_usage AS
SELECT CAST(current_database() AS sql_identifier) AS table_catalog,
CAST(n.nspname AS sql_identifier) AS table_schema,
CAST(c.relname AS sql_identifier) AS table_name,
CAST(ac.attname AS sql_identifier) AS column_name,
CAST(ad.attname AS sql_identifier) AS dependent_column

FROM pg_namespace n, pg_class c, pg_depend d,
pg_attribute ac, pg_attribute ad

WHERE n.oid = c.relnamespace
AND c.oid = ac.attrelid
AND c.oid = ad.attrelid
AND d.classid = 'pg_catalog.pg_class'::regclass
AND d.refclassid = 'pg_catalog.pg_class'::regclass
AND d.objid = d.refobjid
AND c.oid = d.objid
AND d.objsubid = ad.attnum
AND d.refobjsubid = ac.attnum
AND ad.attgenerated <> ''
AND pg_has_role(c.relowner, 'USAGE');

GRANT SELECT ON column_column_usage TO PUBLIC;


/*
@@ -656,7 +678,7 @@ CREATE VIEW columns AS
CAST(c.relname AS sql_identifier) AS table_name,
CAST(a.attname AS sql_identifier) AS column_name,
CAST(a.attnum AS cardinal_number) AS ordinal_position,
CAST(pg_get_expr(ad.adbin, ad.adrelid) AS character_data) AS column_default,
CAST(CASE WHEN a.attgenerated = '' THEN pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS column_default,
CAST(CASE WHEN a.attnotnull OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END
AS yes_or_no)
AS is_nullable,
@@ -745,8 +767,8 @@ CREATE VIEW columns AS
CAST(seq.seqmin AS character_data) AS identity_minimum,
CAST(CASE WHEN seq.seqcycle THEN 'YES' ELSE 'NO' END AS yes_or_no) AS identity_cycle,

CAST('NEVER' AS character_data) AS is_generated,
CAST(null AS character_data) AS generation_expression,
CAST(CASE WHEN a.attgenerated <> '' THEN 'ALWAYS' ELSE 'NEVER' END AS character_data) AS is_generated,
CAST(CASE WHEN a.attgenerated <> '' THEN pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS generation_expression,

CAST(CASE WHEN c.relkind IN ('r', 'p') OR
(c.relkind IN ('v', 'f') AND
@@ -32,6 +32,7 @@
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
#include "executor/nodeModifyTable.h"
#include "executor/tuptable.h"
#include "foreign/fdwapi.h"
#include "libpq/libpq.h"
@@ -2922,6 +2923,21 @@ CopyFrom(CopyState cstate)
}
else
{
/*
* Compute stored generated columns
*
* Switch memory context so that the new tuple is in the same
* context as the old one.
*/
if (resultRelInfo->ri_RelationDesc->rd_att->constr &&
resultRelInfo->ri_RelationDesc->rd_att->constr->has_generated_stored)
{
ExecComputeStoredGenerated(estate, slot);
MemoryContextSwitchTo(batchcontext);
tuple = ExecCopySlotHeapTuple(slot);
MemoryContextSwitchTo(oldcontext);
}

/*
* If the target is a plain table, check the constraints of
* the tuple.
@@ -3271,7 +3287,7 @@ BeginCopyFrom(ParseState *pstate,
fmgr_info(in_func_oid, &in_functions[attnum - 1]);

/* Get default info if needed */
if (!list_member_int(cstate->attnumlist, attnum))
if (!list_member_int(cstate->attnumlist, attnum) && !att->attgenerated)
{
/* attribute is NOT to be copied from input */
/* use default value if one exists */
@@ -4876,6 +4892,11 @@ CopyAttributeOutCSV(CopyState cstate, char *string,
* or NIL if there was none (in which case we want all the non-dropped
* columns).
*
* We don't include generated columns in the generated full list and we don't
* allow them to be specified explicitly. They don't make sense for COPY
* FROM, but we could possibly allow them for COPY TO. But this way it's at
* least ensured that whatever we copy out can be copied back in.
*
* rel can be NULL ... it's only used for error reports.
*/
static List *
@@ -4893,6 +4914,8 @@ CopyGetAttnums(TupleDesc tupDesc, Relation rel, List *attnamelist)
{
if (TupleDescAttr(tupDesc, i)->attisdropped)
continue;
if (TupleDescAttr(tupDesc, i)->attgenerated)
continue;
attnums = lappend_int(attnums, i + 1);
}
}
@@ -4917,6 +4940,12 @@ CopyGetAttnums(TupleDesc tupDesc, Relation rel, List *attnamelist)
continue;
if (namestrcmp(&(att->attname), name) == 0)
{
if (att->attgenerated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
errmsg("column \"%s\" is a generated column",
name),
errdetail("Generated columns cannot be used in COPY.")));
attnum = att->attnum;
break;
}
@@ -760,6 +760,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
rawEnt->attnum = attnum;
rawEnt->raw_default = colDef->raw_default;
rawEnt->missingMode = false;
rawEnt->generated = colDef->generated;
rawDefaults = lappend(rawDefaults, rawEnt);
attr->atthasdef = true;
}
@@ -783,6 +784,9 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,

if (colDef->identity)
attr->attidentity = colDef->identity;

if (colDef->generated)
attr->attgenerated = colDef->generated;
}

/*
@@ -863,6 +867,27 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
*/
rel = relation_open(relationId, AccessExclusiveLock);

/*
* Now add any newly specified column default and generation expressions
* to the new relation. These are passed to us in the form of raw
* parsetrees; we need to transform them to executable expression trees
* before they can be added. The most convenient way to do that is to
* apply the parser's transformExpr routine, but transformExpr doesn't
* work unless we have a pre-existing relation. So, the transformation has
* to be postponed to this final step of CREATE TABLE.
*
* This needs to be before processing the partitioning clauses because
* those could refer to generated columns.
*/
if (rawDefaults)
AddRelationNewConstraints(rel, rawDefaults, NIL,
true, true, false, queryString);

/*
* Make column generation expressions visible for use by partitioning.
*/
CommandCounterIncrement();

/* Process and store partition bound, if any. */
if (stmt->partbound)
{
@@ -1064,16 +1089,12 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
}

/*
* Now add any newly specified column default values and CHECK constraints
* to the new relation. These are passed to us in the form of raw
* parsetrees; we need to transform them to executable expression trees
* before they can be added. The most convenient way to do that is to
* apply the parser's transformExpr routine, but transformExpr doesn't
* work unless we have a pre-existing relation. So, the transformation has
* to be postponed to this final step of CREATE TABLE.
* Now add any newly specified CHECK constraints to the new relation.
* Same as for defaults above, but these need to come after partitioning
* is set up.
*/
if (rawDefaults || stmt->constraints)
AddRelationNewConstraints(rel, rawDefaults, stmt->constraints,
if (stmt->constraints)
AddRelationNewConstraints(rel, NIL, stmt->constraints,
true, true, false, queryString);

ObjectAddressSet(address, RelationRelationId, relationId);
@@ -2252,6 +2273,13 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
def->is_not_null |= attribute->attnotnull;
/* Default and other constraints are handled below */
newattno[parent_attno - 1] = exist_attno;

/* Check for GENERATED conflicts */
if (def->generated != attribute->attgenerated)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("inherited column \"%s\" has a generation conflict",
attributeName)));
}
else
{
@@ -2269,6 +2297,7 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
def->storage = attribute->attstorage;
def->raw_default = NULL;
def->cooked_default = NULL;
def->generated = attribute->attgenerated;
def->collClause = NULL;
def->collOid = attribute->attcollation;
def->constraints = NIL;
@@ -5613,6 +5642,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
attribute.atthasdef = false;
attribute.atthasmissing = false;
attribute.attidentity = colDef->identity;
attribute.attgenerated = colDef->generated;
attribute.attisdropped = false;
attribute.attislocal = colDef->is_local;
attribute.attinhcount = colDef->inhcount;
@@ -5658,7 +5688,9 @@ 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 = true;
rawEnt->missingMode = (!colDef->generated);

rawEnt->generated = colDef->generated;

/*
* This function is intended for CREATE TABLE, so it processes a
@@ -6239,6 +6271,12 @@ ATExecColumnDefault(Relation rel, const char *colName,
colName, RelationGetRelationName(rel)),
newDefault ? 0 : errhint("Use ALTER TABLE ... ALTER COLUMN ... DROP IDENTITY instead.")));

if (TupleDescAttr(tupdesc, attnum - 1)->attgenerated)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("column \"%s\" of relation \"%s\" is a generated column",
colName, RelationGetRelationName(rel))));

/*
* Remove any old default for the column. We use RESTRICT here for
* safety, but at present we do not expect anything to depend on the
@@ -6260,6 +6298,7 @@ ATExecColumnDefault(Relation rel, const char *colName,
rawEnt->attnum = attnum;
rawEnt->raw_default = newDefault;
rawEnt->missingMode = false;
rawEnt->generated = '\0';

/*
* This function is intended for CREATE TABLE, so it processes a
@@ -7560,6 +7599,32 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
*/
checkFkeyPermissions(pkrel, pkattnum, numpks);

/*
* Check some things for generated columns.
*/
for (i = 0; i < numfks; i++)
{
char attgenerated = TupleDescAttr(RelationGetDescr(rel), fkattnum[i] - 1)->attgenerated;

if (attgenerated)
{
/*
* Check restrictions on UPDATE/DELETE actions, per SQL standard
*/
if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETNULL ||
fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT ||
fkconstraint->fk_upd_action == FKCONSTR_ACTION_CASCADE)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("invalid ON UPDATE action for foreign key constraint containing generated column")));
if (fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL ||
fkconstraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("invalid ON DELETE action for foreign key constraint containing generated column")));
}
}

/*
* Look up the equality operators to use in the constraint.
*
@@ -9951,10 +10016,18 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
COERCE_IMPLICIT_CAST,
-1);
if (defaultexpr == NULL)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("default for column \"%s\" cannot be cast automatically to type %s",
colName, format_type_be(targettype))));
{
if (attTup->attgenerated)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("generation expression for column \"%s\" cannot be cast automatically to type %s",
colName, format_type_be(targettype))));
else
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("default for column \"%s\" cannot be cast automatically to type %s",
colName, format_type_be(targettype))));
}
}
else
defaultexpr = NULL;
@@ -10030,6 +10103,21 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
*/
Assert(foundObject.objectSubId == 0);
}
else if (relKind == RELKIND_RELATION &&
foundObject.objectSubId != 0 &&
get_attgenerated(foundObject.objectId, foundObject.objectSubId))
{
/*
* Changing the type of a column that is used by a
* generated column is not allowed by SQL standard.
* It might be doable with some thinking and effort.
*/
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("cannot alter type of a column used by a generated column"),
errdetail("Column \"%s\" is used by generated column \"%s\".",
colName, get_attname(foundObject.objectId, foundObject.objectSubId, false))));
}
else
{
/* Not expecting any other direct dependencies... */
@@ -10174,7 +10262,8 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
/*
* Now scan for dependencies of this column on other things. The only
* thing we should find is the dependency on the column datatype, which we
* want to remove, and possibly a collation dependency.
* want to remove, possibly a collation dependency, and dependencies on
* other columns if it is a generated column.
*/
ScanKeyInit(&key[0],
Anum_pg_depend_classid,
@@ -10195,15 +10284,26 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
while (HeapTupleIsValid(depTup = systable_getnext(scan)))
{
Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(depTup);
ObjectAddress foundObject;

if (foundDep->deptype != DEPENDENCY_NORMAL)
foundObject.classId = foundDep->refclassid;
foundObject.objectId = foundDep->refobjid;
foundObject.objectSubId = foundDep->refobjsubid;

if (foundDep->deptype != DEPENDENCY_NORMAL &&
foundDep->deptype != DEPENDENCY_AUTO)
elog(ERROR, "found unexpected dependency type '%c'",
foundDep->deptype);
if (!(foundDep->refclassid == TypeRelationId &&
foundDep->refobjid == attTup->atttypid) &&
!(foundDep->refclassid == CollationRelationId &&
foundDep->refobjid == attTup->attcollation))
elog(ERROR, "found unexpected dependency for column");
foundDep->refobjid == attTup->attcollation) &&
!(foundDep->refclassid == RelationRelationId &&
foundDep->refobjid == RelationGetRelid(rel) &&
foundDep->refobjsubid != 0)
)
elog(ERROR, "found unexpected dependency for column: %s",
getObjectDescription(&foundObject));

CatalogTupleDelete(depRel, &depTup->t_self);
}
@@ -14267,6 +14367,18 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
pelem->name),
parser_errposition(pstate, pelem->location)));

/*
* Generated columns cannot work: They are computed after BEFORE
* triggers, but partition routing is done before all triggers.
*/
if (attform->attgenerated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("cannot use generated column in partition key"),
errdetail("Column \"%s\" is a generated column.",
pelem->name),
parser_errposition(pstate, pelem->location)));

partattrs[attn] = attform->attnum;
atttype = attform->atttypid;
attcollation = attform->attcollation;
@@ -14354,6 +14466,25 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
errmsg("partition key expressions cannot contain system column references")));
}

/*
* Generated columns cannot work: They are computed after
* BEFORE triggers, but partition routing is done before all
* triggers.
*/
i = -1;
while ((i = bms_next_member(expr_attrs, i)) >= 0)
{
AttrNumber attno = i + FirstLowInvalidHeapAttributeNumber;

if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("cannot use generated column in partition key"),
errdetail("Column \"%s\" is a generated column.",
get_attname(RelationGetRelid(rel), attno, false)),
parser_errposition(pstate, pelem->location)));
}

/*
* While it is not exactly *wrong* for a partition expression
* to be a constant, it seems better to reject such keys.
@@ -75,8 +75,9 @@ static int MyTriggerDepth = 0;
* they use, so we let them be duplicated. Be sure to update all if one needs
* to be changed, however.
*/
#define GetUpdatedColumns(relinfo, estate) \
(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols)
#define GetAllUpdatedColumns(relinfo, estate) \
(bms_union(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols, \
exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->extraUpdatedCols))

/* Local function prototypes */
static void ConvertTriggerToFK(CreateTrigStmt *stmt, Oid funcoid);
@@ -640,6 +641,24 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("BEFORE trigger's WHEN condition cannot reference NEW system columns"),
parser_errposition(pstate, var->location)));
if (TRIGGER_FOR_BEFORE(tgtype) &&
var->varattno == 0 &&
RelationGetDescr(rel)->constr &&
RelationGetDescr(rel)->constr->has_generated_stored)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
errdetail("A whole-row reference is used and the table contains generated columns."),
parser_errposition(pstate, var->location)));
if (TRIGGER_FOR_BEFORE(tgtype) &&
var->varattno > 0 &&
TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attgenerated)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
errdetail("Column \"%s\" is a generated column.",
NameStr(TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attname)),
parser_errposition(pstate, var->location)));
break;
default:
/* can't happen without add_missing_from, so just elog */
@@ -2931,7 +2950,7 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
CMD_UPDATE))
return;

updatedCols = GetUpdatedColumns(relinfo, estate);
updatedCols = GetAllUpdatedColumns(relinfo, estate);

LocTriggerData.type = T_TriggerData;
LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
@@ -2980,7 +2999,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
if (trigdesc && trigdesc->trig_update_after_statement)
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
GetUpdatedColumns(relinfo, estate),
GetAllUpdatedColumns(relinfo, estate),
transition_capture);
}

@@ -3049,7 +3068,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
LocTriggerData.tg_oldtable = NULL;
LocTriggerData.tg_newtable = NULL;
updatedCols = GetUpdatedColumns(relinfo, estate);
updatedCols = GetAllUpdatedColumns(relinfo, estate);
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -3140,7 +3159,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,

AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
GetUpdatedColumns(relinfo, estate),
GetAllUpdatedColumns(relinfo, estate),
transition_capture);
}
}
@@ -918,7 +918,8 @@ DefineDomain(CreateDomainStmt *stmt)
defaultExpr = cookDefault(pstate, constr->raw_expr,
basetypeoid,
basetypeMod,
domainName);
domainName,
0);

/*
* If the expression is just a NULL constant, we treat it
@@ -2228,7 +2229,8 @@ AlterDomainDefault(List *names, Node *defaultRaw)
defaultExpr = cookDefault(pstate, defaultRaw,
typTup->typbasetype,
typTup->typtypmod,
NameStr(typTup->typname));
NameStr(typTup->typname),
0);

/*
* If the expression is just a NULL constant, we treat the command
@@ -102,7 +102,7 @@ static void EvalPlanQualStart(EPQState *epqstate, EState *parentestate,
Plan *planTree);

/*
* Note that GetUpdatedColumns() also exists in commands/trigger.c. There does
* Note that GetAllUpdatedColumns() also exists in commands/trigger.c. There does
* not appear to be any good header to put it into, given the structures that
* it uses, so we let them be duplicated. Be sure to update both if one needs
* to be changed, however.
@@ -111,6 +111,9 @@ static void EvalPlanQualStart(EPQState *epqstate, EState *parentestate,
(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->insertedCols)
#define GetUpdatedColumns(relinfo, estate) \
(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols)
#define GetAllUpdatedColumns(relinfo, estate) \
(bms_union(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols, \
exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->extraUpdatedCols))

/* end of local decls */

@@ -1316,6 +1319,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_FdwState = NULL;
resultRelInfo->ri_usesFdwDirectModify = false;
resultRelInfo->ri_ConstraintExprs = NULL;
resultRelInfo->ri_GeneratedExprs = NULL;
resultRelInfo->ri_junkFilter = NULL;
resultRelInfo->ri_projectReturning = NULL;
resultRelInfo->ri_onConflictArbiterIndexes = NIL;
@@ -2328,7 +2332,7 @@ ExecUpdateLockMode(EState *estate, ResultRelInfo *relinfo)
* been modified, then we can use a weaker lock, allowing for better
* concurrency.
*/
updatedCols = GetUpdatedColumns(relinfo, estate);
updatedCols = GetAllUpdatedColumns(relinfo, estate);
keyCols = RelationGetIndexAttrBitmap(relinfo->ri_RelationDesc,
INDEX_ATTR_BITMAP_KEY);

@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "commands/trigger.h"
#include "executor/executor.h"
#include "executor/nodeModifyTable.h"
#include "nodes/nodeFuncs.h"
#include "parser/parse_relation.h"
#include "parser/parsetree.h"
@@ -412,6 +413,11 @@ ExecSimpleRelationInsert(EState *estate, TupleTableSlot *slot)
{
List *recheckIndexes = NIL;

/* Compute stored generated columns */
if (rel->rd_att->constr &&
rel->rd_att->constr->has_generated_stored)
ExecComputeStoredGenerated(estate, slot);

/* Check the constraints of the tuple */
if (rel->rd_att->constr)
ExecConstraints(resultRelInfo, slot, estate);
@@ -473,6 +479,11 @@ ExecSimpleRelationUpdate(EState *estate, EPQState *epqstate,
List *recheckIndexes = NIL;
bool update_indexes;

/* Compute stored generated columns */
if (rel->rd_att->constr &&
rel->rd_att->constr->has_generated_stored)
ExecComputeStoredGenerated(estate, slot);

/* Check the constraints of the tuple */
if (rel->rd_att->constr)
ExecConstraints(resultRelInfo, slot, estate);
@@ -49,6 +49,7 @@
#include "foreign/fdwapi.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "rewrite/rewriteHandler.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "utils/builtins.h"
@@ -240,6 +241,89 @@ ExecCheckTIDVisible(EState *estate,
ExecClearTuple(tempSlot);
}

/*
* Compute stored generated columns for a tuple
*/
void
ExecComputeStoredGenerated(EState *estate, TupleTableSlot *slot)
{
ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
Relation rel = resultRelInfo->ri_RelationDesc;
TupleDesc tupdesc = RelationGetDescr(rel);
int natts = tupdesc->natts;
MemoryContext oldContext;
Datum *values;
bool *nulls;
bool *replaces;
HeapTuple oldtuple, newtuple;
bool should_free;

Assert(tupdesc->constr && tupdesc->constr->has_generated_stored);

/*
* If first time through for this result relation, build expression
* nodetrees for rel's stored generation expressions. Keep them in the
* per-query memory context so they'll survive throughout the query.
*/
if (resultRelInfo->ri_GeneratedExprs == NULL)
{
oldContext = MemoryContextSwitchTo(estate->es_query_cxt);

resultRelInfo->ri_GeneratedExprs =
(ExprState **) palloc(natts * sizeof(ExprState *));

for (int i = 0; i < natts; i++)
{
if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_STORED)
{
Expr *expr;

expr = (Expr *) build_column_default(rel, i + 1);
if (expr == NULL)
elog(ERROR, "no generation expression found for column number %d of table \"%s\"",
i + 1, RelationGetRelationName(rel));

resultRelInfo->ri_GeneratedExprs[i] = ExecPrepareExpr(expr, estate);
}
}

MemoryContextSwitchTo(oldContext);
}

oldContext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));

values = palloc(sizeof(*values) * natts);
nulls = palloc(sizeof(*nulls) * natts);
replaces = palloc0(sizeof(*replaces) * natts);

for (int i = 0; i < natts; i++)
{
if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_STORED)
{
ExprContext *econtext;
Datum val;
bool isnull;

econtext = GetPerTupleExprContext(estate);
econtext->ecxt_scantuple = slot;

val = ExecEvalExpr(resultRelInfo->ri_GeneratedExprs[i], econtext, &isnull);

values[i] = val;
nulls[i] = isnull;
replaces[i] = true;
}
}

oldtuple = ExecFetchSlotHeapTuple(slot, true, &should_free);
newtuple = heap_modify_tuple(oldtuple, tupdesc, values, nulls, replaces);
ExecForceStoreHeapTuple(newtuple, slot);
if (should_free)
heap_freetuple(oldtuple);

MemoryContextSwitchTo(oldContext);
}

/* ----------------------------------------------------------------
* ExecInsert
*
@@ -297,6 +381,13 @@ ExecInsert(ModifyTableState *mtstate,
}
else if (resultRelInfo->ri_FdwRoutine)
{
/*
* Compute stored generated columns
*/
if (resultRelationDesc->rd_att->constr &&
resultRelationDesc->rd_att->constr->has_generated_stored)
ExecComputeStoredGenerated(estate, slot);

/*
* insert into foreign table: let the FDW do it
*/
@@ -326,6 +417,13 @@ ExecInsert(ModifyTableState *mtstate,
*/
slot->tts_tableOid = RelationGetRelid(resultRelationDesc);

/*
* Compute stored generated columns
*/
if (resultRelationDesc->rd_att->constr &&
resultRelationDesc->rd_att->constr->has_generated_stored)
ExecComputeStoredGenerated(estate, slot);

/*
* Check any RLS WITH CHECK policies.
*
@@ -964,6 +1062,13 @@ ExecUpdate(ModifyTableState *mtstate,
}
else if (resultRelInfo->ri_FdwRoutine)
{
/*
* Compute stored generated columns
*/
if (resultRelationDesc->rd_att->constr &&
resultRelationDesc->rd_att->constr->has_generated_stored)
ExecComputeStoredGenerated(estate, slot);

/*
* update in foreign table: let the FDW do it
*/
@@ -994,6 +1099,13 @@ ExecUpdate(ModifyTableState *mtstate,
*/
slot->tts_tableOid = RelationGetRelid(resultRelationDesc);

/*
* Compute stored generated columns
*/
if (resultRelationDesc->rd_att->constr &&
resultRelationDesc->rd_att->constr->has_generated_stored)
ExecComputeStoredGenerated(estate, slot);

/*
* Check any RLS UPDATE WITH CHECK policies
*
@@ -2390,6 +2390,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
COPY_BITMAPSET_FIELD(selectedCols);
COPY_BITMAPSET_FIELD(insertedCols);
COPY_BITMAPSET_FIELD(updatedCols);
COPY_BITMAPSET_FIELD(extraUpdatedCols);
COPY_NODE_FIELD(securityQuals);

return newnode;
@@ -2888,6 +2889,7 @@ _copyColumnDef(const ColumnDef *from)
COPY_NODE_FIELD(cooked_default);
COPY_SCALAR_FIELD(identity);
COPY_NODE_FIELD(identitySequence);
COPY_SCALAR_FIELD(generated);
COPY_NODE_FIELD(collClause);
COPY_SCALAR_FIELD(collOid);
COPY_NODE_FIELD(constraints);
@@ -2565,6 +2565,7 @@ _equalColumnDef(const ColumnDef *a, const ColumnDef *b)
COMPARE_NODE_FIELD(cooked_default);
COMPARE_SCALAR_FIELD(identity);
COMPARE_NODE_FIELD(identitySequence);
COMPARE_SCALAR_FIELD(generated);
COMPARE_NODE_FIELD(collClause);
COMPARE_SCALAR_FIELD(collOid);
COMPARE_NODE_FIELD(constraints);
@@ -2664,6 +2665,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
COMPARE_BITMAPSET_FIELD(selectedCols);
COMPARE_BITMAPSET_FIELD(insertedCols);
COMPARE_BITMAPSET_FIELD(updatedCols);
COMPARE_BITMAPSET_FIELD(extraUpdatedCols);
COMPARE_NODE_FIELD(securityQuals);

return true;
@@ -2792,6 +2792,7 @@ _outColumnDef(StringInfo str, const ColumnDef *node)
WRITE_NODE_FIELD(cooked_default);
WRITE_CHAR_FIELD(identity);
WRITE_NODE_FIELD(identitySequence);
WRITE_CHAR_FIELD(generated);
WRITE_NODE_FIELD(collClause);
WRITE_OID_FIELD(collOid);
WRITE_NODE_FIELD(constraints);
@@ -3096,6 +3097,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
WRITE_BITMAPSET_FIELD(selectedCols);
WRITE_BITMAPSET_FIELD(insertedCols);
WRITE_BITMAPSET_FIELD(updatedCols);
WRITE_BITMAPSET_FIELD(extraUpdatedCols);
WRITE_NODE_FIELD(securityQuals);
}

@@ -3467,6 +3469,13 @@ _outConstraint(StringInfo str, const Constraint *node)
WRITE_CHAR_FIELD(generated_when);
break;

case CONSTR_GENERATED:
appendStringInfoString(str, "GENERATED");
WRITE_NODE_FIELD(raw_expr);
WRITE_STRING_FIELD(cooked_expr);
WRITE_CHAR_FIELD(generated_when);
break;

case CONSTR_CHECK:
appendStringInfoString(str, "CHECK");
WRITE_BOOL_FIELD(is_no_inherit);
@@ -1430,6 +1430,7 @@ _readRangeTblEntry(void)
READ_BITMAPSET_FIELD(selectedCols);
READ_BITMAPSET_FIELD(insertedCols);
READ_BITMAPSET_FIELD(updatedCols);
READ_BITMAPSET_FIELD(extraUpdatedCols);
READ_NODE_FIELD(securityQuals);

READ_DONE();
@@ -6570,8 +6570,9 @@ make_modifytable(PlannerInfo *root,

/*
* Try to modify the foreign table directly if (1) the FDW provides
* callback functions needed for that, (2) there are no row-level
* triggers on the foreign table, and (3) there are no WITH CHECK
* callback functions needed for that and (2) there are no local
* structures that need to be run for each modified row: row-level
* triggers on the foreign table, stored generated columns, WITH CHECK
* OPTIONs from parent views.
*/
direct_modify = false;
@@ -6581,7 +6582,8 @@ make_modifytable(PlannerInfo *root,
fdwroutine->IterateDirectModify != NULL &&
fdwroutine->EndDirectModify != NULL &&
withCheckOptionLists == NIL &&
!has_row_triggers(subroot, rti, operation))
!has_row_triggers(subroot, rti, operation) &&
!has_stored_generated_columns(subroot, rti))
direct_modify = fdwroutine->PlanDirectModify(subroot, node, rti, i);
if (direct_modify)
direct_modify_plans = bms_add_member(direct_modify_plans, i);
@@ -280,6 +280,10 @@ expand_partitioned_rtentry(PlannerInfo *root, RangeTblEntry *parentrte,
if (!root->partColsUpdated)
root->partColsUpdated =
has_partition_attrs(parentrel, parentrte->updatedCols, NULL);
/*
* There shouldn't be any generated columns in the partition key.
*/
Assert(!has_partition_attrs(parentrel, parentrte->extraUpdatedCols, NULL));

/*
* If the partitioned table has no partitions, treat this as the
@@ -415,6 +419,8 @@ expand_single_inheritance_child(PlannerInfo *root, RangeTblEntry *parentrte,
appinfo->translated_vars);
childrte->updatedCols = translate_col_privs(parentrte->updatedCols,
appinfo->translated_vars);
childrte->extraUpdatedCols = translate_col_privs(parentrte->extraUpdatedCols,
appinfo->translated_vars);
}

/*
@@ -2083,6 +2083,25 @@ has_row_triggers(PlannerInfo *root, Index rti, CmdType event)
return result;
}

bool
has_stored_generated_columns(PlannerInfo *root, Index rti)
{
RangeTblEntry *rte = planner_rt_fetch(rti, root);
Relation relation;
TupleDesc tupdesc;
bool result = false;

/* Assume we already have adequate lock */
relation = heap_open(rte->relid, NoLock);

tupdesc = RelationGetDescr(relation);
result = tupdesc->constr && tupdesc->constr->has_generated_stored;

heap_close(relation, NoLock);

return result;
}

/*
* set_relation_partition_info
*
@@ -2287,6 +2287,7 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
RangeTblEntry *target_rte;
ListCell *orig_tl;
ListCell *tl;
TupleDesc tupdesc = pstate->p_target_relation->rd_att;

tlist = transformTargetList(pstate, origTlist,
EXPR_KIND_UPDATE_SOURCE);
@@ -2345,6 +2346,32 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
if (orig_tl != NULL)
elog(ERROR, "UPDATE target count mismatch --- internal error");

/*
* Record in extraUpdatedCols generated columns referencing updated base
* columns.
*/
if (tupdesc->constr &&
tupdesc->constr->has_generated_stored)
{
for (int i = 0; i < tupdesc->constr->num_defval; i++)
{
AttrDefault defval = tupdesc->constr->defval[i];
Node *expr;
Bitmapset *attrs_used = NULL;

/* skip if not generated column */
if (!TupleDescAttr(tupdesc, defval.adnum - 1)->attgenerated)
continue;

expr = stringToNode(defval.adbin);
pull_varattnos(expr, 1, &attrs_used);

if (bms_overlap(target_rte->updatedCols, attrs_used))
target_rte->extraUpdatedCols = bms_add_member(target_rte->extraUpdatedCols,
defval.adnum - FirstLowInvalidHeapAttributeNumber);
}
}

return tlist;
}

@@ -681,7 +681,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
SAVEPOINT SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES
SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P
START STATEMENT STATISTICS STDIN STDOUT STORAGE STRICT_P STRIP_P
START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P
SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P

TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN
@@ -3497,6 +3497,16 @@ ColConstraintElem:
n->location = @1;
$$ = (Node *)n;
}
| GENERATED generated_when AS '(' a_expr ')' STORED
{
Constraint *n = makeNode(Constraint);
n->contype = CONSTR_GENERATED;
n->generated_when = $2;
n->raw_expr = $5;
n->cooked_expr = NULL;
n->location = @1;
$$ = (Node *)n;
}
| REFERENCES qualified_name opt_column_list key_match key_actions
{
Constraint *n = makeNode(Constraint);
@@ -3587,6 +3597,7 @@ TableLikeOption:
| CONSTRAINTS { $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
| DEFAULTS { $$ = CREATE_TABLE_LIKE_DEFAULTS; }
| IDENTITY_P { $$ = CREATE_TABLE_LIKE_IDENTITY; }
| GENERATED { $$ = CREATE_TABLE_LIKE_GENERATED; }
| INDEXES { $$ = CREATE_TABLE_LIKE_INDEXES; }
| STATISTICS { $$ = CREATE_TABLE_LIKE_STATISTICS; }
| STORAGE { $$ = CREATE_TABLE_LIKE_STORAGE; }
@@ -15234,6 +15245,7 @@ unreserved_keyword:
| STDIN
| STDOUT
| STORAGE
| STORED
| STRICT_P
| STRIP_P
| SUBSCRIPTION
@@ -520,6 +520,14 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
err = _("grouping operations are not allowed in partition key expressions");

break;
case EXPR_KIND_GENERATED_COLUMN:

if (isAgg)
err = _("aggregate functions are not allowed in column generation expressions");
else
err = _("grouping operations are not allowed in column generation expressions");

break;

case EXPR_KIND_CALL_ARGUMENT:
if (isAgg)
@@ -922,6 +930,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_COPY_WHERE:
err = _("window functions are not allowed in COPY FROM WHERE conditions");
break;
case EXPR_KIND_GENERATED_COLUMN:
err = _("window functions are not allowed in column generation expressions");
break;

/*
* There is intentionally no default: case here, so that the
@@ -570,6 +570,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
case EXPR_KIND_PARTITION_EXPRESSION:
case EXPR_KIND_CALL_ARGUMENT:
case EXPR_KIND_COPY_WHERE:
case EXPR_KIND_GENERATED_COLUMN:
/* okay */
break;

@@ -1927,6 +1928,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_COPY_WHERE:
err = _("cannot use subquery in COPY FROM WHERE condition");
break;
case EXPR_KIND_GENERATED_COLUMN:
err = _("cannot use subquery in column generation expression");
break;

/*
* There is intentionally no default: case here, so that the
@@ -3557,6 +3561,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "CALL";
case EXPR_KIND_COPY_WHERE:
return "WHERE";
case EXPR_KIND_GENERATED_COLUMN:
return "GENERATED AS";

/*
* There is intentionally no default: case here, so that the
@@ -2526,6 +2526,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
case EXPR_KIND_COPY_WHERE:
err = _("set-returning functions are not allowed in COPY FROM WHERE conditions");
break;
case EXPR_KIND_GENERATED_COLUMN:
err = _("set-returning functions are not allowed in column generation expressions");
break;

/*
* There is intentionally no default: case here, so that the
@@ -731,6 +731,17 @@ scanRTEForColumn(ParseState *pstate, RangeTblEntry *rte, const char *colname,
colname),
parser_errposition(pstate, location)));

/*
* In generated column, no system column is allowed except tableOid.
*/
if (pstate->p_expr_kind == EXPR_KIND_GENERATED_COLUMN &&
attnum < InvalidAttrNumber && attnum != TableOidAttributeNumber)
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
errmsg("cannot use system column \"%s\" in column generation expression",
colname),
parser_errposition(pstate, location)));

if (attnum != InvalidAttrNumber)
{
/* now check to see if column actually is defined */
@@ -1257,6 +1268,7 @@ addRangeTableEntry(ParseState *pstate,
rte->selectedCols = NULL;
rte->insertedCols = NULL;
rte->updatedCols = NULL;
rte->extraUpdatedCols = NULL;

/*
* Add completed RTE to pstate's range table list, but not to join list
@@ -1328,6 +1340,7 @@ addRangeTableEntryForRelation(ParseState *pstate,
rte->selectedCols = NULL;
rte->insertedCols = NULL;
rte->updatedCols = NULL;
rte->extraUpdatedCols = NULL;

/*
* Add completed RTE to pstate's range table list, but not to join list
@@ -1407,6 +1420,7 @@ addRangeTableEntryForSubquery(ParseState *pstate,
rte->selectedCols = NULL;
rte->insertedCols = NULL;
rte->updatedCols = NULL;
rte->extraUpdatedCols = NULL;

/*
* Add completed RTE to pstate's range table list, but not to join list
@@ -1670,6 +1684,7 @@ addRangeTableEntryForFunction(ParseState *pstate,
rte->selectedCols = NULL;
rte->insertedCols = NULL;
rte->updatedCols = NULL;
rte->extraUpdatedCols = NULL;

/*
* Add completed RTE to pstate's range table list, but not to join list
@@ -1733,6 +1748,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate,
rte->selectedCols = NULL;
rte->insertedCols = NULL;
rte->updatedCols = NULL;
rte->extraUpdatedCols = NULL;

/*
* Add completed RTE to pstate's range table list, but not to join list
@@ -1811,6 +1827,7 @@ addRangeTableEntryForValues(ParseState *pstate,
rte->selectedCols = NULL;
rte->insertedCols = NULL;
rte->updatedCols = NULL;
rte->extraUpdatedCols = NULL;

/*
* Add completed RTE to pstate's range table list, but not to join list
@@ -1881,6 +1898,7 @@ addRangeTableEntryForJoin(ParseState *pstate,
rte->selectedCols = NULL;
rte->insertedCols = NULL;
rte->updatedCols = NULL;
rte->extraUpdatedCols = NULL;

/*
* Add completed RTE to pstate's range table list, but not to join list
@@ -1983,6 +2001,7 @@ addRangeTableEntryForCTE(ParseState *pstate,
rte->selectedCols = NULL;
rte->insertedCols = NULL;
rte->updatedCols = NULL;
rte->extraUpdatedCols = NULL;

/*
* Add completed RTE to pstate's range table list, but not to join list
@@ -502,6 +502,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
bool saw_nullable;
bool saw_default;
bool saw_identity;
bool saw_generated;
ListCell *clist;

cxt->columns = lappend(cxt->columns, column);
@@ -609,6 +610,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
saw_nullable = false;
saw_default = false;
saw_identity = false;
saw_generated = false;

foreach(clist, column->constraints)
{
@@ -689,6 +691,29 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
break;
}

case CONSTR_GENERATED:
if (cxt->ofType)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("generated columns are not supported on typed tables")));
if (cxt->partbound)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("generated columns are not supported on partitions")));

if (saw_generated)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("multiple generation clauses specified for column \"%s\" of table \"%s\"",
column->colname, cxt->relation->relname),
parser_errposition(cxt->pstate,
constraint->location)));
column->generated = ATTRIBUTE_GENERATED_STORED;
column->raw_default = constraint->raw_expr;
Assert(constraint->cooked_expr == NULL);
saw_generated = true;
break;

case CONSTR_CHECK:
cxt->ckconstraints = lappend(cxt->ckconstraints, constraint);
break;
@@ -755,6 +780,22 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
column->colname, cxt->relation->relname),
parser_errposition(cxt->pstate,
constraint->location)));

if (saw_default && saw_generated)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("both default and generation expression specified for column \"%s\" of table \"%s\"",
column->colname, cxt->relation->relname),
parser_errposition(cxt->pstate,
constraint->location)));

if (saw_identity && saw_generated)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("both identity and generation expression specified for column \"%s\" of table \"%s\"",
column->colname, cxt->relation->relname),
parser_errposition(cxt->pstate,
constraint->location)));
}

/*
@@ -983,11 +1024,13 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
* Copy default, if present and the default has been requested
*/
if (attribute->atthasdef &&
(table_like_clause->options & CREATE_TABLE_LIKE_DEFAULTS))
(table_like_clause->options & CREATE_TABLE_LIKE_DEFAULTS ||
table_like_clause->options & CREATE_TABLE_LIKE_GENERATED))
{
Node *this_default = NULL;
AttrDefault *attrdef;
int i;
bool found_whole_row;

/* Find default in constraint structure */
Assert(constr != NULL);
@@ -1002,12 +1045,27 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
}
Assert(this_default != NULL);

def->cooked_default = map_variable_attnos(this_default,
1, 0,
attmap, tupleDesc->natts,
InvalidOid, &found_whole_row);

/*
* If default expr could contain any vars, we'd need to fix 'em,
* but it can't; so default is ready to apply to child.
* Prevent this for the same reason as for constraints below.
* Note that defaults cannot contain any vars, so it's OK that the
* error message refers to generated columns.
*/
if (found_whole_row)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot convert whole-row table reference"),
errdetail("Generation expression for column \"%s\" contains a whole-row reference to table \"%s\".",
attributeName,
RelationGetRelationName(relation))));

def->cooked_default = this_default;
if (attribute->attgenerated &&
(table_like_clause->options & CREATE_TABLE_LIKE_GENERATED))
def->generated = attribute->attgenerated;
}

/*
@@ -453,7 +453,7 @@ logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple)

for (i = 0; i < desc->natts; i++)
{
if (TupleDescAttr(desc, i)->attisdropped)
if (TupleDescAttr(desc, i)->attisdropped || TupleDescAttr(desc, i)->attgenerated)
continue;
nliveatts++;
}
@@ -473,8 +473,7 @@ logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple)
Form_pg_attribute att = TupleDescAttr(desc, i);
char *outputstr;

/* skip dropped columns */
if (att->attisdropped)
if (att->attisdropped || att->attgenerated)
continue;

if (isnull[i])
@@ -573,7 +572,7 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
/* send number of live attributes */
for (i = 0; i < desc->natts; i++)
{
if (TupleDescAttr(desc, i)->attisdropped)
if (TupleDescAttr(desc, i)->attisdropped || TupleDescAttr(desc, i)->attgenerated)
continue;
nliveatts++;
}
@@ -591,7 +590,7 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
Form_pg_attribute att = TupleDescAttr(desc, i);
uint8 flags = 0;

if (att->attisdropped)
if (att->attisdropped || att->attgenerated)
continue;

/* REPLICA IDENTITY FULL means all columns are sent as part of key. */
@@ -276,7 +276,7 @@ logicalrep_rel_open(LogicalRepRelId remoteid, LOCKMODE lockmode)
int attnum;
Form_pg_attribute attr = TupleDescAttr(desc, i);

if (attr->attisdropped)
if (attr->attisdropped || attr->attgenerated)
{
entry->attrmap[i] = -1;
continue;
@@ -697,10 +697,12 @@ fetch_remote_table_info(char *nspname, char *relname,
" LEFT JOIN pg_catalog.pg_index i"
" ON (i.indexrelid = pg_get_replica_identity_index(%u))"
" WHERE a.attnum > 0::pg_catalog.int2"
" AND NOT a.attisdropped"
" AND NOT a.attisdropped %s"
" AND a.attrelid = %u"
" ORDER BY a.attnum",
lrel->remoteid, lrel->remoteid);
lrel->remoteid,
(walrcv_server_version(wrconn) >= 120000 ? "AND a.attgenerated = ''" : ""),
lrel->remoteid);
res = walrcv_exec(wrconn, cmd.data, 4, attrRow);

if (res->status != WALRCV_OK_TUPLES)
@@ -236,7 +236,7 @@ slot_fill_defaults(LogicalRepRelMapEntry *rel, EState *estate,
{
Expr *defexpr;

if (TupleDescAttr(desc, attnum)->attisdropped)
if (TupleDescAttr(desc, attnum)->attisdropped || TupleDescAttr(desc, attnum)->attgenerated)
continue;

if (rel->attrmap[attnum] >= 0)
@@ -276,7 +276,7 @@ maybe_send_schema(LogicalDecodingContext *ctx,
{
Form_pg_attribute att = TupleDescAttr(desc, i);

if (att->attisdropped)
if (att->attisdropped || att->attgenerated)
continue;

if (att->atttypid < FirstNormalObjectId)
@@ -818,6 +818,13 @@ rewriteTargetListIU(List *targetList,

if (att_tup->attidentity == ATTRIBUTE_IDENTITY_BY_DEFAULT && override == OVERRIDING_USER_VALUE)
apply_default = true;

if (att_tup->attgenerated && !apply_default)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("cannot insert into column \"%s\"", NameStr(att_tup->attname)),
errdetail("Column \"%s\" is a generated column.",
NameStr(att_tup->attname))));
}

if (commandType == CMD_UPDATE)
@@ -828,9 +835,23 @@ rewriteTargetListIU(List *targetList,
errmsg("column \"%s\" can only be updated to DEFAULT", NameStr(att_tup->attname)),
errdetail("Column \"%s\" is an identity column defined as GENERATED ALWAYS.",
NameStr(att_tup->attname))));

if (att_tup->attgenerated && new_tle && !apply_default)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("column \"%s\" can only be updated to DEFAULT", NameStr(att_tup->attname)),
errdetail("Column \"%s\" is a generated column.",
NameStr(att_tup->attname))));
}

if (apply_default)
if (att_tup->attgenerated)
{
/*
* stored generated column will be fixed in executor
*/
new_tle = NULL;
}
else if (apply_default)
{
Node *new_expr;

@@ -1137,13 +1158,12 @@ build_column_default(Relation rel, int attrno)
}
}

if (expr == NULL)
{
/*
* No per-column default, so look for a default for the type itself.
*/
/*
* No per-column default, so look for a default for the type itself. But
* not for generated columns.
*/
if (expr == NULL && !att_tup->attgenerated)
expr = get_typdefault(atttype);
}

if (expr == NULL)
return NULL; /* No default anywhere */
@@ -1720,12 +1740,14 @@ ApplyRetrieveRule(Query *parsetree,
subrte->selectedCols = rte->selectedCols;
subrte->insertedCols = rte->insertedCols;
subrte->updatedCols = rte->updatedCols;
subrte->extraUpdatedCols = rte->extraUpdatedCols;

rte->requiredPerms = 0; /* no permission check on subquery itself */
rte->checkAsUser = InvalidOid;
rte->selectedCols = NULL;
rte->insertedCols = NULL;
rte->updatedCols = NULL;
rte->extraUpdatedCols = NULL;

return parsetree;
}
@@ -821,6 +821,39 @@ get_attnum(Oid relid, const char *attname)
return InvalidAttrNumber;
}

/*
* get_attgenerated
*
* Given the relation id and the attribute name,
* return the "attgenerated" field from the attribute relation.
*
* Errors if not found.
*
* Since not generated is represented by '\0', this can also be used as a
* Boolean test.
*/
char
get_attgenerated(Oid relid, AttrNumber attnum)
{
HeapTuple tp;

tp = SearchSysCache2(ATTNUM,
ObjectIdGetDatum(relid),
Int16GetDatum(attnum));
if (HeapTupleIsValid(tp))
{
Form_pg_attribute att_tup = (Form_pg_attribute) GETSTRUCT(tp);
char result;

result = att_tup->attgenerated;
ReleaseSysCache(tp);
return result;
}
else
elog(ERROR, "cache lookup failed for attribute %d of relation %u",
attnum, relid);
}

/*
* get_atttype
*
@@ -27,6 +27,7 @@
#include "nodes/nodeFuncs.h"
#include "optimizer/optimizer.h"
#include "partitioning/partbounds.h"
#include "rewrite/rewriteHandler.h"
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/lsyscache.h"
@@ -515,6 +515,7 @@ RelationBuildTupleDesc(Relation relation)
constr = (TupleConstr *) MemoryContextAlloc(CacheMemoryContext,
sizeof(TupleConstr));
constr->has_not_null = false;
constr->has_generated_stored = false;

/*
* Form a scan key that selects only user attributes (attnum > 0).
@@ -567,6 +568,8 @@ RelationBuildTupleDesc(Relation relation)
/* Update constraint/default info */
if (attp->attnotnull)
constr->has_not_null = true;
if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
constr->has_generated_stored = true;

/* If the column has a default, fill it into the attrdef array */
if (attp->atthasdef)
@@ -3281,6 +3284,7 @@ RelationBuildLocalRelation(const char *relname,
Form_pg_attribute datt = TupleDescAttr(rel->rd_att, i);

datt->attidentity = satt->attidentity;
datt->attgenerated = satt->attgenerated;
datt->attnotnull = satt->attnotnull;
has_not_null |= satt->attnotnull;
}
@@ -2051,6 +2051,11 @@ dumpTableData_insert(Archive *fout, void *dcontext)
{
if (field > 0)
archputs(", ", fout);
if (tbinfo->attgenerated[field])
{
archputs("DEFAULT", fout);
continue;
}
if (PQgetisnull(res, tuple, field))
{
archputs("NULL", fout);
@@ -8219,6 +8224,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
int i_attnotnull;
int i_atthasdef;
int i_attidentity;
int i_attgenerated;
int i_attisdropped;
int i_attlen;
int i_attalign;
@@ -8272,6 +8278,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
"a.attislocal,\n"
"pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n");

if (fout->remoteVersion >= 120000)
appendPQExpBuffer(q,
"a.attgenerated,\n");
else
appendPQExpBuffer(q,
"'' AS attgenerated,\n");

if (fout->remoteVersion >= 110000)
appendPQExpBuffer(q,
"CASE WHEN a.atthasmissing AND NOT a.attisdropped "
@@ -8344,6 +8357,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
i_attnotnull = PQfnumber(res, "attnotnull");
i_atthasdef = PQfnumber(res, "atthasdef");
i_attidentity = PQfnumber(res, "attidentity");
i_attgenerated = PQfnumber(res, "attgenerated");
i_attisdropped = PQfnumber(res, "attisdropped");
i_attlen = PQfnumber(res, "attlen");
i_attalign = PQfnumber(res, "attalign");
@@ -8361,6 +8375,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
tbinfo->attstorage = (char *) pg_malloc(ntups * sizeof(char));
tbinfo->typstorage = (char *) pg_malloc(ntups * sizeof(char));
tbinfo->attidentity = (char *) pg_malloc(ntups * sizeof(char));
tbinfo->attgenerated = (char *) pg_malloc(ntups * sizeof(char));
tbinfo->attisdropped = (bool *) pg_malloc(ntups * sizeof(bool));
tbinfo->attlen = (int *) pg_malloc(ntups * sizeof(int));
tbinfo->attalign = (char *) pg_malloc(ntups * sizeof(char));
@@ -8387,6 +8402,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
tbinfo->attstorage[j] = *(PQgetvalue(res, j, i_attstorage));
tbinfo->typstorage[j] = *(PQgetvalue(res, j, i_typstorage));
tbinfo->attidentity[j] = *(PQgetvalue(res, j, i_attidentity));
tbinfo->attgenerated[j] = *(PQgetvalue(res, j, i_attgenerated));
tbinfo->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS);
tbinfo->attisdropped[j] = (PQgetvalue(res, j, i_attisdropped)[0] == 't');
tbinfo->attlen[j] = atoi(PQgetvalue(res, j, i_attlen));
@@ -15708,6 +15724,20 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
tbinfo->atttypnames[j]);
}

if (has_default)
{
if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
appendPQExpBuffer(q, " GENERATED ALWAYS AS (%s) STORED",
tbinfo->attrdefs[j]->adef_expr);
else
appendPQExpBuffer(q, " DEFAULT %s",
tbinfo->attrdefs[j]->adef_expr);
}


if (has_notnull)
appendPQExpBufferStr(q, " NOT NULL");

/* Add collation if not default for the type */
if (OidIsValid(tbinfo->attcollation[j]))
{
@@ -15718,13 +15748,6 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
appendPQExpBuffer(q, " COLLATE %s",
fmtQualifiedDumpable(coll));
}

if (has_default)
appendPQExpBuffer(q, " DEFAULT %s",
tbinfo->attrdefs[j]->adef_expr);

if (has_notnull)
appendPQExpBufferStr(q, " NOT NULL");
}
}

int numatts = ti->numatts;
char **attnames = ti->attnames;
bool *attisdropped = ti->attisdropped;
char *attgenerated = ti->attgenerated;
bool needComma;
int i;

{
if (attisdropped[i])
continue;
if (attgenerated[i])
continue;
if (needComma)
appendPQExpBufferStr(buffer, ", ");
appendPQExpBufferStr(buffer, fmtId(attnames[i]));
@@ -310,6 +310,7 @@ typedef struct _tableInfo
char *typstorage; /* type storage scheme */
bool *attisdropped; /* true if attr is dropped; don't dump it */
char *attidentity;
char *attgenerated;
int *attlen; /* attribute length, used by binary_upgrade */
char *attalign; /* attribute align, used by binary_upgrade */
bool *attislocal; /* true if attr has local definition */
@@ -1107,6 +1107,16 @@ repairDependencyLoop(DumpableObject **loop,
}
}

/* Loop of table with itself, happens with generated columns */
if (nLoop == 1)
{
if (loop[0]->objType == DO_TABLE)
{
removeObjectDependency(loop[0], loop[0]->dumpId);
return;
}
}

/*
* If all the objects are TABLE_DATA items, what we must have is a
* circular set of foreign key constraints (or a single self-referential
@@ -2392,6 +2392,23 @@
unlike => { exclude_dump_test_schema => 1, },
},

'CREATE TABLE test_table_generated' => {
create_order => 3,
create_sql => 'CREATE TABLE dump_test.test_table_generated (
col1 int primary key,
col2 int generated always as (col1 * 2) stored
);',
regexp => qr/^
\QCREATE TABLE dump_test.test_table_generated (\E\n
\s+\Qcol1 integer NOT NULL,\E\n
\s+\Qcol2 integer GENERATED ALWAYS AS ((col1 * 2)) STORED\E\n
\);
/xms,
like =>
{ %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
unlike => { exclude_dump_test_schema => 1, },
},

'CREATE TABLE table_with_stats' => {
create_order => 98,
create_sql => 'CREATE TABLE dump_test.table_index_stats (
@@ -1464,6 +1464,7 @@ describeOneTableDetails(const char *schemaname,
attnotnull_col = -1,
attcoll_col = -1,
attidentity_col = -1,
attgenerated_col = -1,
isindexkey_col = -1,
indexdef_col = -1,
fdwopts_col = -1,
@@ -1834,8 +1835,9 @@ describeOneTableDetails(const char *schemaname,

if (show_column_details)
{
/* use "pretty" mode for expression to avoid excessive parentheses */
appendPQExpBufferStr(&buf,
",\n (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)"
",\n (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128)"
"\n FROM pg_catalog.pg_attrdef d"
"\n WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef)"
",\n a.attnotnull");
@@ -1852,6 +1854,11 @@ describeOneTableDetails(const char *schemaname,
else
appendPQExpBufferStr(&buf, ",\n ''::pg_catalog.char AS attidentity");
attidentity_col = cols++;
if (pset.sversion >= 120000)
appendPQExpBufferStr(&buf, ",\n a.attgenerated");
else
appendPQExpBufferStr(&buf, ",\n ''::pg_catalog.char AS attgenerated");
attgenerated_col = cols++;
}
if (tableinfo.relkind == RELKIND_INDEX ||
tableinfo.relkind == RELKIND_PARTITIONED_INDEX)
@@ -2032,6 +2039,7 @@ describeOneTableDetails(const char *schemaname,
if (show_column_details)
{
char *identity;
char *generated;
char *default_str = "";

printTableAddCell(&cont, PQgetvalue(res, i, attcoll_col), false, false);
@@ -2041,16 +2049,19 @@ describeOneTableDetails(const char *schemaname,
false, false);

identity = PQgetvalue(res, i, attidentity_col);
generated = PQgetvalue(res, i, attgenerated_col);

if (!identity[0])
/* (note: above we cut off the 'default' string at 128) */
default_str = PQgetvalue(res, i, attrdef_col);
else if (identity[0] == ATTRIBUTE_IDENTITY_ALWAYS)
if (identity[0] == ATTRIBUTE_IDENTITY_ALWAYS)
default_str = "generated always as identity";
else if (identity[0] == ATTRIBUTE_IDENTITY_BY_DEFAULT)
default_str = "generated by default as identity";
else if (generated[0] == ATTRIBUTE_GENERATED_STORED)
default_str = psprintf("generated always as (%s) stored", PQgetvalue(res, i, attrdef_col));
else
/* (note: above we cut off the 'default' string at 128) */
default_str = PQgetvalue(res, i, attrdef_col);

printTableAddCell(&cont, default_str, false, false);
printTableAddCell(&cont, default_str, false, generated[0] ? true : false);
}

/* Info for index columns */
@@ -42,6 +42,7 @@ typedef struct TupleConstr
uint16 num_defval;
uint16 num_check;
bool has_not_null;
bool has_generated_stored;
} TupleConstr;

/*
@@ -53,6 +53,6 @@
*/

/* yyyymmddN */
#define CATALOG_VERSION_NO 201903291
#define CATALOG_VERSION_NO 201903301

#endif
@@ -28,6 +28,7 @@ typedef struct RawColumnDefault
AttrNumber attnum; /* attribute to attach default to */
Node *raw_default; /* default value (untransformed parse tree) */
bool missingMode; /* true if part of add column processing */
char generated; /* attgenerated setting */
} RawColumnDefault;

typedef struct CookedConstraint
@@ -120,7 +121,8 @@ extern Node *cookDefault(ParseState *pstate,
Node *raw_default,
Oid atttypid,
int32 atttypmod,
const char *attname);
const char *attname,
char attgenerated);

extern void DeleteRelationTuple(Oid relid);
extern void DeleteAttributeTuples(Oid relid);
@@ -140,6 +140,9 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
/* One of the ATTRIBUTE_IDENTITY_* constants below, or '\0' */
char attidentity BKI_DEFAULT('\0');

/* One of the ATTRIBUTE_GENERATED_* constants below, or '\0' */
char attgenerated BKI_DEFAULT('\0');

/* Is dropped (ie, logically invisible) or not */
bool attisdropped BKI_DEFAULT(f);

@@ -201,6 +204,8 @@ typedef FormData_pg_attribute *Form_pg_attribute;
#define ATTRIBUTE_IDENTITY_ALWAYS 'a'
#define ATTRIBUTE_IDENTITY_BY_DEFAULT 'd'

#define ATTRIBUTE_GENERATED_STORED 's'

#endif /* EXPOSE_TO_CLIENT_CODE */

#endif /* PG_ATTRIBUTE_H */
@@ -34,7 +34,7 @@
relname => 'pg_attribute', reltype => 'pg_attribute', relam => 'heap',
relfilenode => '0', relpages => '0', reltuples => '0', relallvisible => '0',
reltoastrelid => '0', relhasindex => 'f', relisshared => 'f',
relpersistence => 'p', relkind => 'r', relnatts => '24', relchecks => '0',
relpersistence => 'p', relkind => 'r', relnatts => '25', relchecks => '0',
relhasrules => 'f', relhastriggers => 'f', relhassubclass => 'f',
relrowsecurity => 'f', relforcerowsecurity => 'f', relispopulated => 't',
relreplident => 'n', relispartition => 'f', relfrozenxid => '3',
@@ -15,6 +15,8 @@

#include "nodes/execnodes.h"

extern void ExecComputeStoredGenerated(EState *estate, TupleTableSlot *slot);

extern ModifyTableState *ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags);
extern void ExecEndModifyTable(ModifyTableState *node);
extern void ExecReScanModifyTable(ModifyTableState *node);
@@ -452,6 +452,9 @@ typedef struct ResultRelInfo
/* array of constraint-checking expr states */
ExprState **ri_ConstraintExprs;

/* array of stored generated columns expr states */
ExprState **ri_GeneratedExprs;

/* for removing junk attributes from tuples */
JunkFilter *ri_junkFilter;

@@ -655,6 +655,7 @@ typedef struct ColumnDef
char identity; /* attidentity setting */
RangeVar *identitySequence; /* to store identity sequence name for
* ALTER TABLE ... ADD COLUMN */
char generated; /* attgenerated setting */
CollateClause *collClause; /* untransformed COLLATE spec, if any */
Oid collOid; /* collation OID (InvalidOid if not set) */
List *constraints; /* other constraints on column */
@@ -677,10 +678,11 @@ typedef enum TableLikeOption
CREATE_TABLE_LIKE_COMMENTS = 1 << 0,
CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 1,
CREATE_TABLE_LIKE_DEFAULTS = 1 << 2,
CREATE_TABLE_LIKE_IDENTITY = 1 << 3,
CREATE_TABLE_LIKE_INDEXES = 1 << 4,
CREATE_TABLE_LIKE_STATISTICS = 1 << 5,
CREATE_TABLE_LIKE_STORAGE = 1 << 6,
CREATE_TABLE_LIKE_GENERATED = 1 << 3,
CREATE_TABLE_LIKE_IDENTITY = 1 << 4,
CREATE_TABLE_LIKE_INDEXES = 1 << 5,
CREATE_TABLE_LIKE_STATISTICS = 1 << 6,
CREATE_TABLE_LIKE_STORAGE = 1 << 7,
CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
} TableLikeOption;

@@ -933,6 +935,15 @@ typedef struct PartitionCmd
* them in these fields. A whole-row Var reference is represented by
* setting the bit for InvalidAttrNumber.
*
* updatedCols is also used in some other places, for example, to determine
* which triggers to fire and in FDWs to know which changed columns they
* need to ship off. Generated columns that are caused to be updated by an
* update to a base column are collected in extraUpdatedCols. This is not
* considered for permission checking, but it is useful in those places
* that want to know the full set of columns being updated as opposed to
* only the ones the user explicitly mentioned in the query. (There is
* currently no need for an extraInsertedCols, but it could exist.)
*
* securityQuals is a list of security barrier quals (boolean expressions),
* to be tested in the listed order before returning a row from the
* relation. It is always NIL in parser output. Entries are added by the
@@ -1087,6 +1098,7 @@ typedef struct RangeTblEntry
Bitmapset *selectedCols; /* columns needing SELECT permission */
Bitmapset *insertedCols; /* columns needing INSERT permission */
Bitmapset *updatedCols; /* columns needing UPDATE permission */
Bitmapset *extraUpdatedCols; /* generated columns being updated */
List *securityQuals; /* security barrier quals to apply, if any */
} RangeTblEntry;

@@ -2086,6 +2098,7 @@ typedef enum ConstrType /* types of constraints */
CONSTR_NOTNULL,
CONSTR_DEFAULT,
CONSTR_IDENTITY,
CONSTR_GENERATED,
CONSTR_CHECK,
CONSTR_PRIMARY,
CONSTR_UNIQUE,
@@ -2124,7 +2137,8 @@ typedef struct Constraint
bool is_no_inherit; /* is constraint non-inheritable? */
Node *raw_expr; /* expr, as untransformed parse tree */
char *cooked_expr; /* expr, as nodeToString representation */
char generated_when;
char generated_when; /* ALWAYS or BY DEFAULT */
char generated_kind; /* currently always STORED */

/* Fields used for unique constraints (UNIQUE and PRIMARY KEY): */
List *keys; /* String nodes naming referenced key
@@ -71,4 +71,6 @@ extern double get_function_rows(PlannerInfo *root, Oid funcid, Node *node);

extern bool has_row_triggers(PlannerInfo *root, Index rti, CmdType event);

extern bool has_stored_generated_columns(PlannerInfo *root, Index rti);

#endif /* PLANCAT_H */
@@ -383,6 +383,7 @@ PG_KEYWORD("statistics", STATISTICS, UNRESERVED_KEYWORD)
PG_KEYWORD("stdin", STDIN, UNRESERVED_KEYWORD)
PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD)
PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD)
PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD)
PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD)
PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD)
PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD)
@@ -71,7 +71,8 @@ typedef enum ParseExprKind
EXPR_KIND_PARTITION_BOUND, /* partition bound expression */
EXPR_KIND_PARTITION_EXPRESSION, /* PARTITION BY expression */
EXPR_KIND_CALL_ARGUMENT, /* procedure argument in CALL */
EXPR_KIND_COPY_WHERE /* WHERE condition in COPY FROM */
EXPR_KIND_COPY_WHERE, /* WHERE condition in COPY FROM */
EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */
} ParseExprKind;


@@ -86,6 +86,7 @@ extern Oid get_opfamily_proc(Oid opfamily, Oid lefttype, Oid righttype,
int16 procnum);
extern char *get_attname(Oid relid, AttrNumber attnum, bool missing_ok);
extern AttrNumber get_attnum(Oid relid, const char *attname);
extern char get_attgenerated(Oid relid, AttrNumber attnum);
extern Oid get_atttype(Oid relid, AttrNumber attnum);
extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum,
Oid *typid, int32 *typmod, Oid *collid);
@@ -6,6 +6,10 @@ CREATE TABLE trigger_test (
v varchar,
foo rowcompnest
);
CREATE TABLE trigger_test_generated (
i int,
j int GENERATED ALWAYS AS (i * 2) STORED
);
CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger LANGUAGE plperl AS $$

# make sure keys are sorted for consistent results - perl no longer
@@ -98,6 +102,79 @@ NOTICE: $_TD->{table_name} = 'trigger_test'
NOTICE: $_TD->{table_schema} = 'public'
NOTICE: $_TD->{when} = 'BEFORE'
DROP TRIGGER show_trigger_data_trig on trigger_test;
CREATE TRIGGER show_trigger_data_trig_before
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
FOR EACH ROW EXECUTE PROCEDURE trigger_data();
CREATE TRIGGER show_trigger_data_trig_after
AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
FOR EACH ROW EXECUTE PROCEDURE trigger_data();
insert into trigger_test_generated (i) values (1);
NOTICE: $_TD->{argc} = '0'
NOTICE: $_TD->{event} = 'INSERT'
NOTICE: $_TD->{level} = 'ROW'
NOTICE: $_TD->{name} = 'show_trigger_data_trig_before'
NOTICE: $_TD->{new} = {'i' => '1'}
NOTICE: $_TD->{relid} = 'bogus:12345'
NOTICE: $_TD->{relname} = 'trigger_test_generated'
NOTICE: $_TD->{table_name} = 'trigger_test_generated'
NOTICE: $_TD->{table_schema} = 'public'
NOTICE: $_TD->{when} = 'BEFORE'
NOTICE: $_TD->{argc} = '0'
NOTICE: $_TD->{event} = 'INSERT'
NOTICE: $_TD->{level} = 'ROW'
NOTICE: $_TD->{name} = 'show_trigger_data_trig_after'
NOTICE: $_TD->{new} = {'i' => '1', 'j' => '2'}
NOTICE: $_TD->{relid} = 'bogus:12345'
NOTICE: $_TD->{relname} = 'trigger_test_generated'
NOTICE: $_TD->{table_name} = 'trigger_test_generated'
NOTICE: $_TD->{table_schema} = 'public'
NOTICE: $_TD->{when} = 'AFTER'
update trigger_test_generated set i = 11 where i = 1;
NOTICE: $_TD->{argc} = '0'
NOTICE: $_TD->{event} = 'UPDATE'
NOTICE: $_TD->{level} = 'ROW'
NOTICE: $_TD->{name} = 'show_trigger_data_trig_before'
NOTICE: $_TD->{new} = {'i' => '11'}
NOTICE: $_TD->{old} = {'i' => '1', 'j' => '2'}
NOTICE: $_TD->{relid} = 'bogus:12345'
NOTICE: $_TD->{relname} = 'trigger_test_generated'
NOTICE: $_TD->{table_name} = 'trigger_test_generated'
NOTICE: $_TD->{table_schema} = 'public'
NOTICE: $_TD->{when} = 'BEFORE'
NOTICE: $_TD->{argc} = '0'
NOTICE: $_TD->{event} = 'UPDATE'
NOTICE: $_TD->{level} = 'ROW'
NOTICE: $_TD->{name} = 'show_trigger_data_trig_after'
NOTICE: $_TD->{new} = {'i' => '11', 'j' => '22'}
NOTICE: $_TD->{old} = {'i' => '1', 'j' => '2'}
NOTICE: $_TD->{relid} = 'bogus:12345'
NOTICE: $_TD->{relname} = 'trigger_test_generated'
NOTICE: $_TD->{table_name} = 'trigger_test_generated'
NOTICE: $_TD->{table_schema} = 'public'
NOTICE: $_TD->{when} = 'AFTER'
delete from trigger_test_generated;
NOTICE: $_TD->{argc} = '0'
NOTICE: $_TD->{event} = 'DELETE'
NOTICE: $_TD->{level} = 'ROW'
NOTICE: $_TD->{name} = 'show_trigger_data_trig_before'
NOTICE: $_TD->{old} = {'i' => '11', 'j' => '22'}
NOTICE: $_TD->{relid} = 'bogus:12345'
NOTICE: $_TD->{relname} = 'trigger_test_generated'
NOTICE: $_TD->{table_name} = 'trigger_test_generated'
NOTICE: $_TD->{table_schema} = 'public'
NOTICE: $_TD->{when} = 'BEFORE'
NOTICE: $_TD->{argc} = '0'
NOTICE: $_TD->{event} = 'DELETE'
NOTICE: $_TD->{level} = 'ROW'
NOTICE: $_TD->{name} = 'show_trigger_data_trig_after'
NOTICE: $_TD->{old} = {'i' => '11', 'j' => '22'}
NOTICE: $_TD->{relid} = 'bogus:12345'
NOTICE: $_TD->{relname} = 'trigger_test_generated'
NOTICE: $_TD->{table_name} = 'trigger_test_generated'
NOTICE: $_TD->{table_schema} = 'public'
NOTICE: $_TD->{when} = 'AFTER'
DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;
insert into trigger_test values(1,'insert', '("(1)")');
CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
CREATE TRIGGER show_trigger_data_trig
@@ -295,3 +372,21 @@ NOTICE: perlsnitch: ddl_command_start DROP TABLE
NOTICE: perlsnitch: ddl_command_end DROP TABLE
drop event trigger perl_a_snitch;
drop event trigger perl_b_snitch;
-- dealing with generated columns
CREATE FUNCTION generated_test_func1() RETURNS trigger
LANGUAGE plperl
AS $$
$_TD->{new}{j} = 5; # not allowed
return 'MODIFY';
$$;
CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
TRUNCATE trigger_test_generated;
INSERT INTO trigger_test_generated (i) VALUES (1);
ERROR: cannot set generated column "j"
CONTEXT: PL/Perl function "generated_test_func1"
SELECT * FROM trigger_test_generated;
i | j
---+---
(0 rows)

@@ -266,7 +266,7 @@ static plperl_proc_desc *compile_plperl_function(Oid fn_oid,
bool is_trigger,
bool is_event_trigger);

static SV *plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc);
static SV *plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc, bool include_generated);
static SV *plperl_hash_from_datum(Datum attr);
static SV *plperl_ref_from_pg_array(Datum arg, Oid typid);
static SV *split_array(plperl_array_info *info, int first, int last, int nest);
@@ -1644,21 +1644,28 @@ plperl_trigger_build_args(FunctionCallInfo fcinfo)
hv_store_string(hv, "name", cstr2sv(tdata->tg_trigger->tgname));
hv_store_string(hv, "relid", cstr2sv(relid));

/*
* Note: In BEFORE trigger, stored generated columns are not computed yet,
* so don't make them accessible in NEW row.
*/

if (TRIGGER_FIRED_BY_INSERT(tdata->tg_event))
{
event = "INSERT";
if (TRIGGER_FIRED_FOR_ROW(tdata->tg_event))
hv_store_string(hv, "new",
plperl_hash_from_tuple(tdata->tg_trigtuple,
tupdesc));
tupdesc,
!TRIGGER_FIRED_BEFORE(tdata->tg_event)));
}
else if (TRIGGER_FIRED_BY_DELETE(tdata->tg_event))
{
event = "DELETE";
if (TRIGGER_FIRED_FOR_ROW(tdata->tg_event))
hv_store_string(hv, "old",
plperl_hash_from_tuple(tdata->tg_trigtuple,
tupdesc));
tupdesc,
true));
}
else if (TRIGGER_FIRED_BY_UPDATE(tdata->tg_event))
{
@@ -1667,10 +1674,12 @@ plperl_trigger_build_args(FunctionCallInfo fcinfo)
{
hv_store_string(hv, "old",
plperl_hash_from_tuple(tdata->tg_trigtuple,
tupdesc));
tupdesc,
true));
hv_store_string(hv, "new",
plperl_hash_from_tuple(tdata->tg_newtuple,
tupdesc));
tupdesc,
!TRIGGER_FIRED_BEFORE(tdata->tg_event)));
}
}
else if (TRIGGER_FIRED_BY_TRUNCATE(tdata->tg_event))
@@ -1791,6 +1800,11 @@ plperl_modify_tuple(HV *hvTD, TriggerData *tdata, HeapTuple otup)
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot set system attribute \"%s\"",
key)));
if (attr->attgenerated)
ereport(ERROR,
(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
errmsg("cannot set generated column \"%s\"",
key)));

modvalues[attn - 1] = plperl_sv_to_datum(val,
attr->atttypid,
@@ -3012,15 +3026,15 @@ plperl_hash_from_datum(Datum attr)
tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
tmptup.t_data = td;

sv = plperl_hash_from_tuple(&tmptup, tupdesc);
sv = plperl_hash_from_tuple(&tmptup, tupdesc, true);
ReleaseTupleDesc(tupdesc);

return sv;
}

/* Build a hash from all attributes of a given tuple. */
static SV *
plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc)
plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc, bool include_generated)
{
dTHX;
HV *hv;
@@ -3044,6 +3058,13 @@ plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc)
if (att->attisdropped)
continue;

if (att->attgenerated)
{
/* don't include unless requested */
if (!include_generated)
continue;
}

attname = NameStr(att->attname);
attr = heap_getattr(tuple, i + 1, tupdesc, &isnull);

@@ -3198,7 +3219,7 @@ plperl_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 processed,
av_extend(rows, processed);
for (i = 0; i < processed; i++)
{
row = plperl_hash_from_tuple(tuptable->vals[i], tuptable->tupdesc);
row = plperl_hash_from_tuple(tuptable->vals[i], tuptable->tupdesc, true);
av_push(rows, row);
}
hv_store_string(result, "rows",
@@ -3484,7 +3505,8 @@ plperl_spi_fetchrow(char *cursor)
else
{
row = plperl_hash_from_tuple(SPI_tuptable->vals[0],
SPI_tuptable->tupdesc);
SPI_tuptable->tupdesc,
true);
}
SPI_freetuptable(SPI_tuptable);
}
@@ -8,6 +8,11 @@ CREATE TABLE trigger_test (
foo rowcompnest
);

CREATE TABLE trigger_test_generated (
i int,
j int GENERATED ALWAYS AS (i * 2) STORED
);

CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger LANGUAGE plperl AS $$

# make sure keys are sorted for consistent results - perl no longer
@@ -70,6 +75,21 @@ delete from trigger_test;

DROP TRIGGER show_trigger_data_trig on trigger_test;

CREATE TRIGGER show_trigger_data_trig_before
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
FOR EACH ROW EXECUTE PROCEDURE trigger_data();

CREATE TRIGGER show_trigger_data_trig_after
AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
FOR EACH ROW EXECUTE PROCEDURE trigger_data();

insert into trigger_test_generated (i) values (1);
update trigger_test_generated set i = 11 where i = 1;
delete from trigger_test_generated;

DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;

insert into trigger_test values(1,'insert', '("(1)")');
CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;

@@ -221,3 +241,19 @@ drop table foo;

drop event trigger perl_a_snitch;
drop event trigger perl_b_snitch;

-- dealing with generated columns

CREATE FUNCTION generated_test_func1() RETURNS trigger
LANGUAGE plperl
AS $$
$_TD->{new}{j} = 5; # not allowed
return 'MODIFY';
$$;

CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();

TRUNCATE trigger_test_generated;
INSERT INTO trigger_test_generated (i) VALUES (1);
SELECT * FROM trigger_test_generated;
@@ -924,6 +924,26 @@ plpgsql_exec_trigger(PLpgSQL_function *func,
false, false);
expanded_record_set_tuple(rec_old->erh, trigdata->tg_trigtuple,
false, false);

/*
* In BEFORE trigger, stored generated columns are not computed yet,
* so make them null in the NEW row. (Only needed in UPDATE branch;
* in the INSERT case, they are already null, but in UPDATE, the field
* still contains the old value.) Alternatively, we could construct a
* whole new row structure without the generated columns, but this way
* seems more efficient and potentially less confusing.
*/
if (tupdesc->constr && tupdesc->constr->has_generated_stored &&
TRIGGER_FIRED_BEFORE(trigdata->tg_event))
{
for (int i = 0; i < tupdesc->natts; i++)
if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_STORED)
expanded_record_set_field_internal(rec_new->erh,
i + 1,
(Datum) 0,
true, /*isnull*/
false, false);
}
}
else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
{
@@ -67,6 +67,10 @@ SELECT * FROM users;
-- dump trigger data
CREATE TABLE trigger_test
(i int, v text );
CREATE TABLE trigger_test_generated (
i int,
j int GENERATED ALWAYS AS (i * 2) STORED
);
CREATE FUNCTION trigger_data() RETURNS trigger LANGUAGE plpythonu AS $$

if 'relid' in TD:
@@ -203,6 +207,77 @@ NOTICE: TD[when] => BEFORE
DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
DROP TRIGGER show_trigger_data_trig_before on trigger_test;
DROP TRIGGER show_trigger_data_trig_after on trigger_test;
CREATE TRIGGER show_trigger_data_trig_before
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
FOR EACH ROW EXECUTE PROCEDURE trigger_data();
CREATE TRIGGER show_trigger_data_trig_after
AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
FOR EACH ROW EXECUTE PROCEDURE trigger_data();
insert into trigger_test_generated (i) values (1);
NOTICE: TD[args] => None
NOTICE: TD[event] => INSERT
NOTICE: TD[level] => ROW
NOTICE: TD[name] => show_trigger_data_trig_before
NOTICE: TD[new] => {'i': 1}
NOTICE: TD[old] => None
NOTICE: TD[relid] => bogus:12345
NOTICE: TD[table_name] => trigger_test_generated
NOTICE: TD[table_schema] => public
NOTICE: TD[when] => BEFORE
NOTICE: TD[args] => None
NOTICE: TD[event] => INSERT
NOTICE: TD[level] => ROW
NOTICE: TD[name] => show_trigger_data_trig_after
NOTICE: TD[new] => {'i': 1, 'j': 2}
NOTICE: TD[old] => None
NOTICE: TD[relid] => bogus:12345
NOTICE: TD[table_name] => trigger_test_generated
NOTICE: TD[table_schema] => public
NOTICE: TD[when] => AFTER
update trigger_test_generated set i = 11 where i = 1;
NOTICE: TD[args] => None
NOTICE: TD[event] => UPDATE
NOTICE: TD[level] => ROW
NOTICE: TD[name] => show_trigger_data_trig_before
NOTICE: TD[new] => {'i': 11}
NOTICE: TD[old] => {'i': 1, 'j': 2}
NOTICE: TD[relid] => bogus:12345
NOTICE: TD[table_name] => trigger_test_generated
NOTICE: TD[table_schema] => public
NOTICE: TD[when] => BEFORE
NOTICE: TD[args] => None
NOTICE: TD[event] => UPDATE
NOTICE: TD[level] => ROW
NOTICE: TD[name] => show_trigger_data_trig_after
NOTICE: TD[new] => {'i': 11, 'j': 22}
NOTICE: TD[old] => {'i': 1, 'j': 2}
NOTICE: TD[relid] => bogus:12345
NOTICE: TD[table_name] => trigger_test_generated
NOTICE: TD[table_schema] => public
NOTICE: TD[when] => AFTER
delete from trigger_test_generated;
NOTICE: TD[args] => None
NOTICE: TD[event] => DELETE
NOTICE: TD[level] => ROW
NOTICE: TD[name] => show_trigger_data_trig_before
NOTICE: TD[new] => None
NOTICE: TD[old] => {'i': 11, 'j': 22}
NOTICE: TD[relid] => bogus:12345
NOTICE: TD[table_name] => trigger_test_generated
NOTICE: TD[table_schema] => public
NOTICE: TD[when] => BEFORE
NOTICE: TD[args] => None
NOTICE: TD[event] => DELETE
NOTICE: TD[level] => ROW
NOTICE: TD[name] => show_trigger_data_trig_after
NOTICE: TD[new] => None
NOTICE: TD[old] => {'i': 11, 'j': 22}
NOTICE: TD[relid] => bogus:12345
NOTICE: TD[table_name] => trigger_test_generated
NOTICE: TD[table_schema] => public
NOTICE: TD[when] => AFTER
DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;
insert into trigger_test values(1,'insert');
CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
CREATE TRIGGER show_trigger_data_trig
@@ -524,3 +599,22 @@ INFO: old: 1 -> a
INFO: new: 1 -> b
DROP TABLE transition_table_test;
DROP FUNCTION transition_table_test_f();
-- dealing with generated columns
CREATE FUNCTION generated_test_func1() RETURNS trigger
LANGUAGE plpythonu
AS $$
TD['new']['j'] = 5 # not allowed
return 'MODIFY'
$$;
CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
TRUNCATE trigger_test_generated;
INSERT INTO trigger_test_generated (i) VALUES (1);
ERROR: cannot set generated column "j"
CONTEXT: while modifying trigger row
PL/Python function "generated_test_func1"
SELECT * FROM trigger_test_generated;
i | j
---+---
(0 rows)

@@ -357,7 +357,7 @@ PLy_cursor_iternext(PyObject *self)
exec_ctx->curr_proc);

ret = PLy_input_from_tuple(&cursor->result, SPI_tuptable->vals[0],
SPI_tuptable->tupdesc);
SPI_tuptable->tupdesc, true);
}

SPI_freetuptable(SPI_tuptable);
@@ -453,7 +453,8 @@ PLy_cursor_fetch(PyObject *self, PyObject *args)
{
PyObject *row = PLy_input_from_tuple(&cursor->result,
SPI_tuptable->vals[i],
SPI_tuptable->tupdesc);
SPI_tuptable->tupdesc,
true);

PyList_SetItem(ret->rows, i, row);
}