diff --git a/.gitignore b/.gitignore index 1695c70b7..b9ac8b3f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.gitee/ /.vscode/ /.idea/ +.vscode diff --git a/contrib/postgres_fdw/postgres_fdw.cpp b/contrib/postgres_fdw/postgres_fdw.cpp index 072f3a0e4..4bfdbb344 100755 --- a/contrib/postgres_fdw/postgres_fdw.cpp +++ b/contrib/postgres_fdw/postgres_fdw.cpp @@ -967,9 +967,10 @@ static List *postgresPlanForeignModify(PlannerInfo *root, ModifyTable *plan, Ind } } else if (operation == CMD_UPDATE) { Bitmapset *tmpset = bms_copy(rte->updatedCols); + Bitmapset *allUpdatedCols = bms_union(tmpset, rte->extraUpdatedCols); AttrNumber col; - while ((col = bms_first_member(tmpset)) >= 0) { + while ((col = bms_first_member(allUpdatedCols)) >= 0) { col += FirstLowInvalidHeapAttributeNumber; if (col <= InvalidAttrNumber) { /* shouldn't happen */ elog(ERROR, "system-column update is not supported"); diff --git a/doc/src/sgml/ref/alter_table.sgmlin b/doc/src/sgml/ref/alter_table.sgmlin index f7438b522..c844bb931 100755 --- a/doc/src/sgml/ref/alter_table.sgmlin +++ b/doc/src/sgml/ref/alter_table.sgmlin @@ -70,6 +70,7 @@ where column_constraint can be: NULL | CHECK ( expression ) | DEFAULT default_expr | + GENERATED ALWAYS AS ( generation_expr ) STORED | UNIQUE index_parameters | PRIMARY KEY index_parameters | REFERENCES reftable [ ( refcolumn ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] diff --git a/doc/src/sgml/ref/create_table.sgmlin b/doc/src/sgml/ref/create_table.sgmlin index 59de07c7c..c82ef2920 100644 --- a/doc/src/sgml/ref/create_table.sgmlin +++ b/doc/src/sgml/ref/create_table.sgmlin @@ -30,6 +30,7 @@ where column_constraint can be: NULL | CHECK ( expression ) | DEFAULT default_expr | + GENERATED ALWAYS AS ( generation_expr ) STORED | UNIQUE index_parameters | PRIMARY KEY index_parameters | REFERENCES reftable [ ( refcolumn ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] @@ -47,7 +48,7 @@ where table_constraint can be: where compress_mode can be: { DELTA | PREFIX | DICTIONARY | NUMSTR | NOCOMPRESS } where like_option can be: -{ INCLUDING | EXCLUDING } { DEFAULTS | CONSTRAINTS | INDEXES | STORAGE | COMMENTS | PARTITION | RELOPTIONS | DISTRIBUTION | ALL } +{ INCLUDING | EXCLUDING } { DEFAULTS | GENERATED | CONSTRAINTS | INDEXES | STORAGE | COMMENTS | PARTITION | RELOPTIONS | DISTRIBUTION | ALL } where index_parameters can be: [ WITH ( {storage_parameter = value} [, ... ] ) ] [ USING INDEX TABLESPACE tablespace_name ] diff --git a/doc/src/sgml/ref/create_table_partition.sgmlin b/doc/src/sgml/ref/create_table_partition.sgmlin index e129a8232..1ffd87932 100644 --- a/doc/src/sgml/ref/create_table_partition.sgmlin +++ b/doc/src/sgml/ref/create_table_partition.sgmlin @@ -34,6 +34,7 @@ where column_constraint can be: NULL | CHECK ( expression ) | DEFAULT default_expr | + GENERATED ALWAYS AS ( generation_expr ) STORED | UNIQUE index_parameters | PRIMARY KEY index_parameters | REFERENCES reftable [ ( refcolumn ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] @@ -51,7 +52,7 @@ where index_parameters can be: [ WITH ( {storage_parameter = value} [, ... ] ) ] [ USING INDEX TABLESPACE tablespace_name ] where like_option can be: -{ INCLUDING | EXCLUDING } { DEFAULTS | CONSTRAINTS | INDEXES | STORAGE | COMMENTS | RELOPTIONS | DISTRIBUTION | ALL } +{ INCLUDING | EXCLUDING } { DEFAULTS | GENERATED | CONSTRAINTS | INDEXES | STORAGE | COMMENTS | RELOPTIONS | DISTRIBUTION | ALL } where partition_less_than_item can be: PARTITION partition_name VALUES LESS THAN ( { partition_value | MAXVALUE } ) [TABLESPACE tablespace_name] where partition_start_end_item can be: diff --git a/src/bin/pg_dump/pg_dump.cpp b/src/bin/pg_dump/pg_dump.cpp index 4d6856c82..c88dda147 100644 --- a/src/bin/pg_dump/pg_dump.cpp +++ b/src/bin/pg_dump/pg_dump.cpp @@ -61,6 +61,7 @@ #include "catalog/pg_rlspolicy.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_attrdef.h" #include "libpq/libpq-fs.h" #include "libpq/libpq-int.h" #include "catalog/pgxc_node.h" @@ -2426,6 +2427,11 @@ static int dumpTableData_insert(Archive* fout, void* dcontext) for (field = 0; field < nfields; field++) { if (field > 0) archprintf(fout, ", "); + if (tbinfo->attrdefs[field] && + tbinfo->attrdefs[field]->generatedCol) { + (void)archputs("DEFAULT", fout); + continue; + } if (PQgetisnull(res, tuple, field)) { archprintf(fout, "NULL"); continue; @@ -8281,6 +8287,8 @@ void getTableAttrs(Archive* fout, TableInfo* tblinfo, int numTables) if (hasdefaults) { AttrDefInfo* attrdefs = NULL; int numDefaults; + ArchiveHandle* AH = (ArchiveHandle*)fout; + bool hasGenColFeature = is_column_exists(AH->connection, AttrDefaultRelationId, "adgencol"); if (g_verbose) write_msg(NULL, @@ -8288,10 +8296,19 @@ void getTableAttrs(Archive* fout, TableInfo* tblinfo, int numTables) fmtQualifiedId(fout, tbinfo->dobj.nmspace->dobj.name, tbinfo->dobj.name)); resetPQExpBuffer(q); - if (fout->remoteVersion >= 70300) { + if (hasGenColFeature) { appendPQExpBuffer(q, "SELECT tableoid, oid, adnum, " "pg_catalog.pg_get_expr(adbin, adrelid) AS adsrc " + ", adgencol AS generatedCol " + "FROM pg_catalog.pg_attrdef " + "WHERE adrelid = '%u'::pg_catalog.oid", + tbinfo->dobj.catId.oid); + } else if (fout->remoteVersion >= 70300) { + appendPQExpBuffer(q, + "SELECT tableoid, oid, adnum, " + "pg_catalog.pg_get_expr(adbin, adrelid) AS adsrc " + ", '' AS generatedCol " "FROM pg_catalog.pg_attrdef " "WHERE adrelid = '%u'::pg_catalog.oid", tbinfo->dobj.catId.oid); @@ -8300,6 +8317,7 @@ void getTableAttrs(Archive* fout, TableInfo* tblinfo, int numTables) appendPQExpBuffer(q, "SELECT tableoid, 0 AS oid, adnum, " "pg_get_expr(adbin, adrelid) AS adsrc " + ", '' AS generatedCol " "FROM pg_attrdef " "WHERE adrelid = '%u'::oid", tbinfo->dobj.catId.oid); @@ -8307,6 +8325,7 @@ void getTableAttrs(Archive* fout, TableInfo* tblinfo, int numTables) /* no pg_get_expr, so must rely on adsrc */ appendPQExpBuffer(q, "SELECT tableoid, oid, adnum, adsrc " + ", '' AS generatedCol " "FROM pg_attrdef " "WHERE adrelid = '%u'::oid", tbinfo->dobj.catId.oid); @@ -8316,6 +8335,7 @@ void getTableAttrs(Archive* fout, TableInfo* tblinfo, int numTables) "SELECT " "(SELECT oid FROM pg_class WHERE relname = 'pg_attrdef') AS tableoid, " "oid, adnum, adsrc " + ", '' AS generatedCol " "FROM pg_attrdef " "WHERE adrelid = '%u'::oid", tbinfo->dobj.catId.oid); @@ -8346,6 +8366,7 @@ void getTableAttrs(Archive* fout, TableInfo* tblinfo, int numTables) AssignDumpId(&attrdefs[j].dobj); attrdefs[j].adtable = tbinfo; attrdefs[j].adnum = adnum; + attrdefs[j].generatedCol = *(PQgetvalue(res, j, 4)); attrdefs[j].adef_expr = gs_strdup(PQgetvalue(res, j, 3)); attrdefs[j].dobj.name = gs_strdup(tbinfo->dobj.name); @@ -17290,7 +17311,11 @@ static void dumpTableSchema(Archive* fout, TableInfo* tbinfo) default_value = (char *)plaintext; } #endif - appendPQExpBuffer(q, " DEFAULT %s", default_value); + if (tbinfo->attrdefs[j]->generatedCol == ATTRIBUTE_GENERATED_STORED) + appendPQExpBuffer(q, " GENERATED ALWAYS AS (%s) STORED", + default_value); + else + appendPQExpBuffer(q, " DEFAULT %s", default_value); } if (has_notnull) appendPQExpBuffer(q, " NOT NULL"); @@ -20448,10 +20473,15 @@ static const char* fmtCopyColumnList(const TableInfo* ti) appendPQExpBuffer(q, "("); for (i = 0; i < numatts; i++) { - if (attisdropped[i]) + if (attisdropped[i]) { continue; - if (needComma) + } + if (ti->attrdefs[i] != NULL && ti->attrdefs[i]->generatedCol) { + continue; + } + if (needComma) { appendPQExpBuffer(q, ", "); + } appendPQExpBuffer(q, "%s", fmtId(attnames[i])); needComma = true; } diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 8461727da..70ebfdb20 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -274,6 +274,7 @@ typedef struct _attrDefInfo { int adnum; char* adef_expr; /* decompiled DEFAULT expression */ bool separate; /* TRUE if must dump as separate item */ + char generatedCol; } AttrDefInfo; typedef struct _tableDataInfo { diff --git a/src/bin/pg_dump/pg_dump_sort.cpp b/src/bin/pg_dump/pg_dump_sort.cpp index 55c4e435c..d91c407a0 100644 --- a/src/bin/pg_dump/pg_dump_sort.cpp +++ b/src/bin/pg_dump/pg_dump_sort.cpp @@ -935,6 +935,22 @@ static void repairDependencyLoop(DumpableObject** loop, int nLoop) if (true == repairDependencyDomainConstraintLoops(loop, nLoop)) return; + /* + * Loop of table with itself --- just ignore it. + * + * (Actually, what this arises from is a dependency of a table column on + * another column, which happens with generated columns; or a dependency + * of a table column on the whole table, which happens with partitioning. + * But we didn't pay attention to sub-object IDs while collecting the + * dependency data, so we can't see that here.) + */ + 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 diff --git a/src/bin/psql/describe.cpp b/src/bin/psql/describe.cpp index 61cc86d88..2f9904769 100644 --- a/src/bin/psql/describe.cpp +++ b/src/bin/psql/describe.cpp @@ -17,6 +17,7 @@ #include "catalog/pg_class.h" #include "catalog/pg_default_acl.h" +#include "catalog/pg_attrdef.h" #include "common.h" #include "describe.h" #include "dumputils.h" @@ -1278,6 +1279,7 @@ static bool describeOneTableDetails(const char* schemaname, const char* relation bool show_modifiers = false; bool retval = false; bool hasreplident = is_column_exists(pset.db, RelationRelationId, "relreplident"); + bool hasGenColFeature = is_column_exists(pset.db, AttrDefaultRelationId, "adgencol"); bool has_rlspolicy = false; const char* has_rlspolicy_sql = @@ -1511,6 +1513,14 @@ static bool describeOneTableDetails(const char* schemaname, const char* relation } appendPQExpBuffer(&buf, ",\n a.attkvtype"); } + if (hasGenColFeature) { + appendPQExpBuffer(&buf, ",\n (SELECT h.adgencol" + "\n FROM pg_catalog.pg_attrdef h" + "\n WHERE h.adrelid = a.attrelid AND h.adnum = a.attnum AND a.atthasdef)" + " AS generated_column"); + } else { + appendPQExpBuffer(&buf, ", '' AS generated_column "); + } appendPQExpBuffer(&buf, "\nFROM pg_catalog.pg_attribute a"); appendPQExpBuffer(&buf, "\nWHERE a.attrelid = '%s' AND a.attnum > 0 AND NOT a.attisdropped AND " "a.attkvtype != 4 AND a.attname <> 'tableoid'", oid); @@ -1682,7 +1692,11 @@ static bool describeOneTableDetails(const char* schemaname, const char* relation appendPQExpBufferStr(&tmpbuf, " "); } /* translator: default values of column definitions */ - appendPQExpBuffer(&tmpbuf, _("default %s"), default_value); + if (strlen(PQgetvalue(res, i, PQfnumber(res, "generated_column"))) > 0) { + appendPQExpBuffer(&tmpbuf, _("generated always as (%s) stored"), default_value); + } else { + appendPQExpBuffer(&tmpbuf, _("default %s"), default_value); + } } #ifdef HAVE_CE if (hasFullEncryptFeature && diff --git a/src/common/backend/catalog/heap.cpp b/src/common/backend/catalog/heap.cpp index 4a2456d37..ec629cdcf 100644 --- a/src/common/backend/catalog/heap.cpp +++ b/src/common/backend/catalog/heap.cpp @@ -77,6 +77,7 @@ #include "parser/parse_collate.h" #include "parser/parse_expr.h" #include "parser/parse_relation.h" +#include "parser/parsetree.h" #include "pgxc/groupmgr.h" #include "storage/buf/buf.h" #include "storage/predicate.h" @@ -157,6 +158,9 @@ static Oid binary_upgrade_get_next_part_toast_pg_class_oid(); static Oid binary_upgrade_get_next_part_toast_pg_class_rfoid(); static Oid binary_upgrade_get_next_part_pg_partition_rfoid(); static void LockSeqConstraints(Relation rel, List* Constraints); +static bool ReferenceGenerated(const List *rawdefaultlist, AttrNumber attnum); +static bool CheckNestedGeneratedWalker(Node *node, ParseState *context); +static bool CheckNestedGenerated(ParseState *pstate, Node *node); extern void getErrorTableFilePath(char* buf, int len, Oid databaseid, Oid reid); extern void make_tmptable_cache_key(Oid relNode); @@ -3468,13 +3472,13 @@ void heap_drop_with_catalog(Oid relid) /* * Store a default expression for column attnum of relation rel. */ -void StoreAttrDefault(Relation rel, AttrNumber attnum, Node* expr) +void StoreAttrDefault(Relation rel, AttrNumber attnum, Node* expr, char generatedCol) { char* adbin = NULL; char* adsrc = NULL; Relation adrel; HeapTuple tuple; - Datum values[4]; + Datum values[Natts_pg_attrdef]; Relation attrrel; HeapTuple atttup; Form_pg_attribute attStruct; @@ -3499,6 +3503,9 @@ void StoreAttrDefault(Relation rel, AttrNumber attnum, Node* expr) values[Anum_pg_attrdef_adnum - 1] = attnum; values[Anum_pg_attrdef_adbin - 1] = CStringGetTextDatum(adbin); values[Anum_pg_attrdef_adsrc - 1] = CStringGetTextDatum(adsrc); + if (t_thrd.proc->workingVersionNum >= GENERATED_COL_VERSION_NUM) { + values[Anum_pg_attrdef_adgencol - 1] = CharGetDatum(generatedCol); + } adrel = heap_open(AttrDefaultRelationId, RowExclusiveLock); @@ -3555,7 +3562,22 @@ void StoreAttrDefault(Relation rel, AttrNumber attnum, Node* expr) /* * Record dependencies on objects used in the expression, too. */ - recordDependencyOnExpr(&defobject, expr, NIL, DEPENDENCY_NORMAL); + if (generatedCol == ATTRIBUTE_GENERATED_STORED) + { + /* + * Generated column: Dropping anything that the generation expression + * refers to automatically drops the generated column. + */ + recordDependencyOnSingleRelExpr(&colobject, expr, RelationGetRelid(rel), + DEPENDENCY_AUTO, + DEPENDENCY_AUTO); + } else { + /* + * Normal default: Dropping anything that the default refers to + * requires CASCADE and drops the default only. + */ + recordDependencyOnExpr(&defobject, expr, NIL, DEPENDENCY_NORMAL); + } } /* @@ -3678,7 +3700,10 @@ static void StoreConstraints(Relation rel, List* cooked_constraints) switch (con->contype) { case CONSTR_DEFAULT: - StoreAttrDefault(rel, con->attnum, con->expr); + StoreAttrDefault(rel, con->attnum, con->expr, 0); + break; + case CONSTR_GENERATED: + StoreAttrDefault(rel, con->attnum, con->expr, ATTRIBUTE_GENERATED_STORED); break; case CONSTR_CHECK: StoreRelCheck( @@ -3754,6 +3779,8 @@ List* AddRelationNewConstraints( rte = addRangeTableEntryForRelation(pstate, rel, NULL, false, true); addRTEtoQuery(pstate, rte, true, true, true); + pstate->p_rawdefaultlist = newColDefaults; + /* * Process column default expressions. */ @@ -3761,12 +3788,14 @@ List* AddRelationNewConstraints( RawColumnDefault* colDef = (RawColumnDefault*)lfirst(cell); Form_pg_attribute atp = rel->rd_att->attrs[colDef->attnum - 1]; - expr = cookDefault(pstate, colDef->raw_default, atp->atttypid, atp->atttypmod, NameStr(atp->attname)); + expr = cookDefault(pstate, colDef->raw_default, atp->atttypid, atp->atttypmod, NameStr(atp->attname), + colDef->generatedCol); /* * 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 @@ -3774,10 +3803,10 @@ List* AddRelationNewConstraints( * critical because the column default needs to be retained to * override any default that the domain might have. */ - if (expr == NULL || (IsA(expr, Const) && ((Const*)expr)->constisnull)) + if (expr == NULL || (!colDef->generatedCol && IsA(expr, Const) && ((Const *)expr)->constisnull)) continue; - StoreAttrDefault(rel, colDef->attnum, expr); + StoreAttrDefault(rel, colDef->attnum, expr, colDef->generatedCol); cooked = (CookedConstraint*)palloc(sizeof(CookedConstraint)); cooked->contype = CONSTR_DEFAULT; @@ -3790,6 +3819,8 @@ List* AddRelationNewConstraints( cooked->is_no_inherit = false; cookedConstraints = lappend(cookedConstraints, cooked); } + + pstate->p_rawdefaultlist = NIL; /* * Process constraint expressions. */ @@ -4045,6 +4076,54 @@ static void SetRelationNumChecks(Relation rel, int numchecks) heap_close(relrel, RowExclusiveLock); } +static bool ReferenceGenerated(const List *rawdefaultlist, AttrNumber attnum) +{ + ListCell *cell = NULL; + + foreach (cell, rawdefaultlist) { + RawColumnDefault *colDef = (RawColumnDefault *)lfirst(cell); + if (colDef->attnum == attnum && colDef->generatedCol) { + return true; + } + } + + return false; +} + +/* + * Check for references to generated columns + */ +static bool CheckNestedGeneratedWalker(Node *node, ParseState *context) +{ + ParseState *pstate = context; + if (node == NULL) { + return false; + } else if (IsA(node, Var)) { + Var *var = (Var *)node; + RangeTblEntry *rte = rt_fetch(var->varno, pstate->p_rtable); + Oid relid = rte->relid; + AttrNumber attnum = var->varattno; + + if (OidIsValid(relid) && AttributeNumberIsValid(attnum) && + (ReferenceGenerated(pstate->p_rawdefaultlist, attnum) || GetGenerated(relid, attnum))) { + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use generated column \"%s\" in column generation expression", + get_attname(relid, attnum)), + errdetail("A generated column cannot reference another generated column."), + parser_errposition(pstate, var->location))); + } + + return false; + } else { + return expression_tree_walker(node, (bool (*)())CheckNestedGeneratedWalker, context); + } +} + +static bool CheckNestedGenerated(ParseState *pstate, Node *node) +{ + return CheckNestedGeneratedWalker(node, pstate); +} + Node* parseParamRef(ParseState* pstate, ParamRef* pref) { Node* param_expr = NULL; @@ -4087,7 +4166,8 @@ Node* parseParamRef(ParseState* pstate, ParamRef* pref) * type (and typmod atttypmod). attname is only needed in this case: * it is used in the error message, if any. */ -Node* cookDefault(ParseState* pstate, Node* raw_default, Oid atttypid, int32 atttypmod, char* attname) +Node *cookDefault(ParseState *pstate, Node *raw_default, Oid atttypid, int32 atttypmod, char *attname, + char generatedCol) { Node* expr = NULL; @@ -4099,7 +4179,18 @@ Node* cookDefault(ParseState* pstate, Node* raw_default, Oid atttypid, int32 att /* * Transform raw parsetree to executable expression. */ + pstate->p_expr_kind = generatedCol ? EXPR_KIND_GENERATED_COLUMN : EXPR_KIND_COLUMN_DEFAULT; expr = transformExpr(pstate, raw_default); + pstate->p_expr_kind = EXPR_KIND_NONE; + + if (generatedCol == ATTRIBUTE_GENERATED_STORED) + { + (void)CheckNestedGenerated(pstate, expr); + + if (contain_mutable_functions(expr)) + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("generation expression is not immutable"))); + } /* Version control for DDL PBE */ if (t_thrd.proc->workingVersionNum >= DDL_PBE_VERSION_NUM) @@ -4107,7 +4198,7 @@ Node* cookDefault(ParseState* pstate, Node* raw_default, Oid atttypid, int32 att /* * Make sure default expr does not refer to any vars. */ - if (contain_var_clause(expr)) + if (contain_var_clause(expr) && !generatedCol) ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("cannot use column references in default expression"))); @@ -4121,19 +4212,21 @@ Node* cookDefault(ParseState* pstate, Node* raw_default, Oid atttypid, int32 att * It can't return a set either. */ if (expression_returns_set(expr)) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("default expression must not return a set"))); + ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("%s expression must not return a set", generatedCol ? "generated column" : "default"))); /* * No subplans or aggregates, either... */ if (pstate->p_hasSubLinks) - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot use subquery in default expression"))); + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot use subquery in %s expression", generatedCol ? "generated column" : "default"))); if (pstate->p_hasAggs) - ereport( - ERROR, (errcode(ERRCODE_GROUPING_ERROR), errmsg("cannot use aggregate function in default expression"))); + ereport(ERROR, (errcode(ERRCODE_GROUPING_ERROR), + errmsg("cannot use aggregate function in %s expression", generatedCol ? "generated column" : "default"))); if (pstate->p_hasWindowFuncs) - ereport(ERROR, (errcode(ERRCODE_WINDOWING_ERROR), errmsg("cannot use window function in default expression"))); + ereport(ERROR, (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("cannot use window function in %s expression", generatedCol ? "generated column" : "default"))); /* * Coerce the expression to the correct type and typmod, if given. This @@ -4143,17 +4236,13 @@ Node* cookDefault(ParseState* pstate, Node* raw_default, Oid atttypid, int32 att if (OidIsValid(atttypid)) { Oid type_id = exprType(expr); - expr = coerce_to_target_type( - pstate, expr, type_id, atttypid, atttypmod, COERCION_ASSIGNMENT, COERCE_IMPLICIT_CAST, -1); + expr = coerce_to_target_type(pstate, expr, type_id, atttypid, atttypmod, COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, -1); if (expr == NULL) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("column \"%s\" is of type %s" - " but default expression is of type %s", - attname, - format_type_be(atttypid), - format_type_be(type_id)), - errhint("You will need to rewrite or cast the expression."))); + ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" is of type %s but %s expression is of type %s", attname, format_type_be(atttypid), + generatedCol ? "generated column" : "default", format_type_be(type_id)), + errhint("You will need to rewrite or cast the expression."))); } /* diff --git a/src/common/backend/catalog/information_schema.sql b/src/common/backend/catalog/information_schema.sql index c3d76edb4..a1603c60d 100644 --- a/src/common/backend/catalog/information_schema.sql +++ b/src/common/backend/catalog/information_schema.sql @@ -648,7 +648,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 ad.adgencol <> 's' 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, @@ -737,8 +737,8 @@ CREATE VIEW columns AS CAST(null AS character_data) AS identity_minimum, CAST(null 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 ad.adgencol = 's' THEN 'ALWAYS' ELSE 'NEVER' END AS character_data) AS is_generated, + CAST(CASE WHEN ad.adgencol = 's' THEN pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS generation_expression, CAST(CASE WHEN c.relkind = 'r' OR (c.relkind = 'v' diff --git a/src/common/backend/nodes/copyfuncs.cpp b/src/common/backend/nodes/copyfuncs.cpp index 1f183924a..2acc183cb 100644 --- a/src/common/backend/nodes/copyfuncs.cpp +++ b/src/common/backend/nodes/copyfuncs.cpp @@ -3382,6 +3382,7 @@ static RangeTblEntry* _copyRangeTblEntry(const RangeTblEntry* from) COPY_BITMAPSET_FIELD(modifiedCols); COPY_BITMAPSET_FIELD(insertedCols); COPY_BITMAPSET_FIELD(updatedCols); + COPY_BITMAPSET_FIELD(extraUpdatedCols); COPY_SCALAR_FIELD(orientation); COPY_STRING_FIELD(mainRelName); COPY_STRING_FIELD(mainRelNameSpace); @@ -3817,6 +3818,7 @@ static ColumnDef* _copyColumnDef(const ColumnDef* from) COPY_SCALAR_FIELD(storage); COPY_SCALAR_FIELD(cmprs_mode); COPY_NODE_FIELD(raw_default); + COPY_SCALAR_FIELD(generatedCol); COPY_NODE_FIELD(cooked_default); COPY_NODE_FIELD(collClause); COPY_SCALAR_FIELD(collOid); diff --git a/src/common/backend/nodes/equalfuncs.cpp b/src/common/backend/nodes/equalfuncs.cpp index 28739e478..05a415510 100644 --- a/src/common/backend/nodes/equalfuncs.cpp +++ b/src/common/backend/nodes/equalfuncs.cpp @@ -2428,6 +2428,7 @@ static bool _equalColumnDef(const ColumnDef* a, const ColumnDef* b) COMPARE_SCALAR_FIELD(cmprs_mode); COMPARE_NODE_FIELD(raw_default); COMPARE_NODE_FIELD(cooked_default); + COMPARE_SCALAR_FIELD(generatedCol); COMPARE_NODE_FIELD(collClause); COMPARE_NODE_FIELD(clientLogicColumnRef); COMPARE_SCALAR_FIELD(collOid); @@ -2527,6 +2528,7 @@ static bool _equalRangeTblEntry(const RangeTblEntry* a, const RangeTblEntry* b) COMPARE_BITMAPSET_FIELD(selectedCols); COMPARE_BITMAPSET_FIELD(insertedCols); COMPARE_BITMAPSET_FIELD(updatedCols); + COMPARE_BITMAPSET_FIELD(extraUpdatedCols); COMPARE_SCALAR_FIELD(orientation); COMPARE_NODE_FIELD(securityQuals); COMPARE_SCALAR_FIELD(subquery_pull_up); diff --git a/src/common/backend/nodes/outfuncs.cpp b/src/common/backend/nodes/outfuncs.cpp index e20235767..1c25fd72f 100644 --- a/src/common/backend/nodes/outfuncs.cpp +++ b/src/common/backend/nodes/outfuncs.cpp @@ -3637,6 +3637,10 @@ static void _outColumnDef(StringInfo str, ColumnDef* node) WRITE_NODE_FIELD(constraints); WRITE_NODE_FIELD(fdwoptions); WRITE_NODE_FIELD(clientLogicColumnRef); + if (t_thrd.proc->workingVersionNum >= GENERATED_COL_VERSION_NUM) { + if (node->generatedCol) + WRITE_CHAR_FIELD(generatedCol); + } } static void _outTypeName(StringInfo str, TypeName* node) @@ -4172,6 +4176,52 @@ static void _outSetOperationStmt(StringInfo str, SetOperationStmt* node) WRITE_TYPEINFO_LIST(colTypes); } +static void _outRteRelation(StringInfo str, const RangeTblEntry *node) +{ + WRITE_OID_FIELD(relid); + WRITE_CHAR_FIELD(relkind); + if (t_thrd.proc->workingVersionNum >= MATVIEW_VERSION_NUM) { + WRITE_BOOL_FIELD(isResultRel); + } + WRITE_NODE_FIELD(tablesample); + WRITE_OID_FIELD(partitionOid); + WRITE_BOOL_FIELD(isContainPartition); + if (t_thrd.proc->workingVersionNum >= SYNONYM_VERSION_NUM) { + WRITE_OID_FIELD(refSynOid); + } + WRITE_BOOL_FIELD(ispartrel); + WRITE_BOOL_FIELD(ignoreResetRelid); + WRITE_NODE_FIELD(pname); + WRITE_NODE_FIELD(partid_list); + WRITE_NODE_FIELD(plist); + if (node->relid >= FirstBootstrapObjectId && IsStatisfyUpdateCompatibility(node->relid) && + node->mainRelName == NULL) { + /* + * For inherit table, the relname will be different + */ + // shipping out for dn, relid will be different on dn, so relname and relnamespace string will be must. + + char *rteRelname = NULL; + char *rteRelnamespace = NULL; + + getNameById(node->relid, "RTE", &rteRelnamespace, &rteRelname); + + appendStringInfo(str, " :relname "); + _outToken(str, rteRelname); + appendStringInfo(str, " :relnamespace "); + _outToken(str, rteRelnamespace); + + /* + * Same reason as above, + * we need to serialize the namespace and synonym name instead of refSynOid + * when relation name is referenced from one synonym, + */ + if (t_thrd.proc->workingVersionNum >= SYNONYM_VERSION_NUM) { + WRITE_SYNINFO_FIELD(refSynOid); + } + } +} + static void _outRangeTblEntry(StringInfo str, RangeTblEntry* node) { WRITE_NODE_TYPE("RTE"); @@ -4192,48 +4242,7 @@ static void _outRangeTblEntry(StringInfo str, RangeTblEntry* node) switch (node->rtekind) { case RTE_RELATION: - WRITE_OID_FIELD(relid); - WRITE_CHAR_FIELD(relkind); - if (t_thrd.proc->workingVersionNum >= MATVIEW_VERSION_NUM) { - WRITE_BOOL_FIELD(isResultRel); - } - WRITE_NODE_FIELD(tablesample); - WRITE_OID_FIELD(partitionOid); - WRITE_BOOL_FIELD(isContainPartition); - if (t_thrd.proc->workingVersionNum >= SYNONYM_VERSION_NUM) { - WRITE_OID_FIELD(refSynOid); - } - WRITE_BOOL_FIELD(ispartrel); - WRITE_BOOL_FIELD(ignoreResetRelid); - WRITE_NODE_FIELD(pname); - WRITE_NODE_FIELD(partid_list); - WRITE_NODE_FIELD(plist); - if (node->relid >= FirstBootstrapObjectId && IsStatisfyUpdateCompatibility(node->relid) && - node->mainRelName == NULL) { - /* - * For inherit table, the relname will be different - */ - // shipping out for dn, relid will be different on dn, so relname and relnamespace string will be must. - - char* rte_relname = NULL; - char* rte_relnamespace = NULL; - - getNameById(node->relid, "RTE", &rte_relnamespace, &rte_relname); - - appendStringInfo(str, " :relname "); - _outToken(str, rte_relname); - appendStringInfo(str, " :relnamespace "); - _outToken(str, rte_relnamespace); - - /* - * Same reason as above, - * we need to serialize the namespace and synonym name instead of refSynOid - * when relation name is referenced from one synonym, - */ - if (t_thrd.proc->workingVersionNum >= SYNONYM_VERSION_NUM) { - WRITE_SYNINFO_FIELD(refSynOid); - } - } + _outRteRelation(str, node); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); @@ -4302,6 +4311,10 @@ static void _outRangeTblEntry(StringInfo str, RangeTblEntry* node) if (t_thrd.proc->workingVersionNum >= SUBLINKPULLUP_VERSION_NUM) { WRITE_BOOL_FIELD(sublink_pull_up); } + + if (t_thrd.proc->workingVersionNum >= GENERATED_COL_VERSION_NUM) { + WRITE_BITMAPSET_FIELD(extraUpdatedCols); + } } /* diff --git a/src/common/backend/nodes/readfuncs.cpp b/src/common/backend/nodes/readfuncs.cpp index a4ac803cc..759feb1d5 100644 --- a/src/common/backend/nodes/readfuncs.cpp +++ b/src/common/backend/nodes/readfuncs.cpp @@ -2821,6 +2821,11 @@ static RangeTblEntry* _readRangeTblEntry(void) READ_BOOL_FIELD(sublink_pull_up); } + IF_EXIST(extraUpdatedCols) + { + READ_BITMAPSET_FIELD(extraUpdatedCols); + } + READ_DONE(); } @@ -4823,6 +4828,10 @@ static ColumnDef* _readColumnDef() local_node->storage = 0; } + IF_EXIST(generatedCol) { + READ_CHAR_FIELD(generatedCol); + } + READ_DONE(); } diff --git a/src/common/backend/parser/analyze.cpp b/src/common/backend/parser/analyze.cpp index cf5d21947..e8dd15118 100644 --- a/src/common/backend/parser/analyze.cpp +++ b/src/common/backend/parser/analyze.cpp @@ -3284,6 +3284,8 @@ static List* transformUpdateTargetList(ParseState* pstate, List* qryTlist, List* ERROR, (errcode(ERRCODE_NOT_NULL_VIOLATION), errmsg("UPDATE target count mismatch --- internal error"))); } + setExtraUpdatedCols(pstate); + return tlist; } diff --git a/src/common/backend/parser/gram.y b/src/common/backend/parser/gram.y index bb769e5f9..a7d62e3e0 100755 --- a/src/common/backend/parser/gram.y +++ b/src/common/backend/parser/gram.y @@ -730,7 +730,7 @@ static void parameter_check_execute_direct(const char* query); FALSE_P FAMILY FAST FENCED FETCH FILEHEADER_P FILL_MISSING_FIELDS FILTER FIRST_P FIXED_P FLOAT_P FOLLOWING FOR FORCE FOREIGN FORMATTER FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS - GLOBAL GLOBAL_FUNCTION GRANT GRANTED GREATEST GROUP_P GROUPING_P + GENERATED GLOBAL GLOBAL_FUNCTION GRANT GRANTED GREATEST GROUP_P GROUPING_P HANDLER HAVING HDFSDIRECTORY HEADER_P HOLD HOUR_P @@ -770,7 +770,7 @@ static void parameter_check_execute_direct(const char* query); SAVEPOINT SCHEMA SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHIPPABLE SHOW SHUTDOWN SIMILAR SIMPLE SIZE SLICE SMALLDATETIME SMALLDATETIME_FORMAT_P SMALLINT SNAPSHOT SOME SOURCE_P SPACE SPILL SPLIT STABLE STANDALONE_P START - STATEMENT STATEMENT_ID STATISTICS STDIN STDOUT STORAGE STORE_P STREAM STRICT_P STRIP_P SUBSTRING + STATEMENT STATEMENT_ID STATISTICS STDIN STDOUT STORAGE STORE_P STORED STREAM STRICT_P STRIP_P SUBSTRING SYMMETRIC SYNONYM SYSDATE SYSID SYSTEM_P SYS_REFCURSOR TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THAN THEN TIME TIME_FORMAT_P TIMESTAMP TIMESTAMP_FORMAT_P TIMESTAMPDIFF TINYINT @@ -851,7 +851,7 @@ static void parameter_check_execute_direct(const char* query); * blame any funny behavior of UNBOUNDED on the SQL standard, though. */ %nonassoc UNBOUNDED /* ideally should have same precedence as IDENT */ -%nonassoc IDENT NULL_P PARTITION RANGE ROWS PRECEDING FOLLOWING CUBE ROLLUP +%nonassoc IDENT GENERATED NULL_P PARTITION RANGE ROWS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %nonassoc NOTNULL %nonassoc ISNULL @@ -5090,6 +5090,20 @@ ColConstraintElem: n->cooked_expr = NULL; $$ = (Node *)n; } + | GENERATED ALWAYS AS '(' a_expr ')' STORED + { +#ifdef ENABLE_MULTIPLE_NODES + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Generated column is not yet supported."))); +#endif + Constraint *n = makeNode(Constraint); + n->contype = CONSTR_GENERATED; + n->generated_when = ATTRIBUTE_IDENTITY_ALWAYS; + n->raw_expr = $5; + n->cooked_expr = NULL; + n->location = @1; + $$ = (Node *)n; + } | REFERENCES qualified_name opt_column_list key_match key_actions { #ifdef ENABLE_MULTIPLE_NODES @@ -5230,6 +5244,7 @@ TableLikeIncludingOption: | RELOPTIONS { $$ = CREATE_TABLE_LIKE_RELOPTIONS; } | DISTRIBUTION { $$ = CREATE_TABLE_LIKE_DISTRIBUTION; } | OIDS { $$ = CREATE_TABLE_LIKE_OIDS;} + | GENERATED { $$ = CREATE_TABLE_LIKE_GENERATED; } ; TableLikeExcludingOption: @@ -5242,6 +5257,7 @@ TableLikeExcludingOption: | RELOPTIONS { $$ = CREATE_TABLE_LIKE_RELOPTIONS; } | DISTRIBUTION { $$ = CREATE_TABLE_LIKE_DISTRIBUTION; } | OIDS { $$ = CREATE_TABLE_LIKE_OIDS; } + | GENERATED { $$ = CREATE_TABLE_LIKE_GENERATED; } | ALL { $$ = CREATE_TABLE_LIKE_ALL; } ; @@ -20184,6 +20200,7 @@ unreserved_keyword: | FORWARD | FUNCTION | FUNCTIONS + | GENERATED | GLOBAL | GLOBAL_FUNCTION | GRANTED @@ -20390,6 +20407,7 @@ unreserved_keyword: | STDOUT | STORAGE | STORE_P + | STORED | STREAM | STRICT_P | STRIP_P diff --git a/src/common/backend/parser/parse_merge.cpp b/src/common/backend/parser/parse_merge.cpp index 201e19f18..1b7879506 100644 --- a/src/common/backend/parser/parse_merge.cpp +++ b/src/common/backend/parser/parse_merge.cpp @@ -640,6 +640,38 @@ void fix_merge_stmt_for_insert_update(ParseState* pstate, MergeStmt* stmt) fill_join_expr(pstate, stmt); } +void setExtraUpdatedCols(ParseState* pstate) +{ + TupleDesc tupdesc = pstate->p_target_relation->rd_att; + RangeTblEntry* target_rte = pstate->p_target_rangetblentry; + + /* + * 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 (!defval.generatedCol) + continue; + + expr = (Node *)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); + } + } +} + /* * transformUpdateTargetList - * handle SET clause in UPDATE clause @@ -716,6 +748,8 @@ static List* transformUpdateTargetList(ParseState* pstate, List* origTlist) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("UPDATE target count mismatch --- internal error"))); } + setExtraUpdatedCols(pstate); + return tlist; } diff --git a/src/common/backend/parser/parse_node.cpp b/src/common/backend/parser/parse_node.cpp index 0541aac55..4bb77bcdf 100644 --- a/src/common/backend/parser/parse_node.cpp +++ b/src/common/backend/parser/parse_node.cpp @@ -56,6 +56,7 @@ ParseState* make_parsestate(ParseState* parentParseState) pstate->p_resolve_unknowns = true; pstate->ignoreplus = false; pstate->p_plusjoin_rte_info = NULL; + pstate->p_rawdefaultlist = NIL; if (parentParseState != NULL) { pstate->p_sourcetext = parentParseState->p_sourcetext; diff --git a/src/common/backend/parser/parse_relation.cpp b/src/common/backend/parser/parse_relation.cpp index 4d4f40382..5af444876 100644 --- a/src/common/backend/parser/parse_relation.cpp +++ b/src/common/backend/parser/parse_relation.cpp @@ -90,6 +90,7 @@ static void set_rte_flags(RangeTblEntry* rte, bool inh, bool inFromCl, int perm) rte->selectedCols = NULL; rte->insertedCols = NULL; rte->updatedCols = NULL; + rte->extraUpdatedCols = NULL; } /* @@ -566,6 +567,15 @@ Node* scanRTEForColumn(ParseState* pstate, RangeTblEntry* rte, char* colname, in if (rte->rtekind == RTE_RELATION && rte->relkind != RELKIND_FOREIGN_TABLE && rte->relkind != RELKIND_STREAM) { /* quick check to see if name could be a system column */ attnum = specialAttNum(colname); + + /* + * In generated column, no system column is allowed except tableOid. + */ + if (pstate->p_expr_kind == EXPR_KIND_GENERATED_COLUMN && attnum < InvalidAttrNumber) { + ereport(ERROR, (errmodule(MOD_GEN_COL), 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. Because of @@ -1735,6 +1745,7 @@ RangeTblEntry* addRangeTableEntryForCTE( rte->selectedCols = NULL; rte->insertedCols = NULL; rte->updatedCols = NULL; + rte->extraUpdatedCols = NULL; rte->orientation = REL_ORIENT_UNKNOWN; /* @@ -1791,6 +1802,7 @@ RangeTblEntry* getRangeTableEntryByRelation(Relation rel) rte->insertedCols = NULL; rte->updatedCols = NULL; rte->lateral = false; + rte->extraUpdatedCols = NULL; setRteOrientation(rel, rte); diff --git a/src/common/backend/parser/parse_utilcmd.cpp b/src/common/backend/parser/parse_utilcmd.cpp index 58558e134..797129d8f 100644 --- a/src/common/backend/parser/parse_utilcmd.cpp +++ b/src/common/backend/parser/parse_utilcmd.cpp @@ -351,6 +351,7 @@ List* transformCreateStmt(CreateStmt* stmt, const char* queryString, const List* cxt.bucketOid = InvalidOid; cxt.relnodelist = NULL; cxt.toastnodelist = NULL; + cxt.ofType = (stmt->ofTypename != NULL); /* We have gen uuids, so use it */ if (stmt->uuids != NIL) @@ -772,6 +773,7 @@ static void transformColumnDefinition(CreateStmtContext* cxt, ColumnDef* column, bool is_serial = false; bool saw_nullable = false; bool saw_default = false; + bool saw_generated = false; Constraint* constraint = NULL; ListCell* clist = NULL; ClientLogicColumnRef* clientLogicColumnRef = NULL; @@ -888,6 +890,23 @@ static void transformColumnDefinition(CreateStmtContext* cxt, ColumnDef* column, saw_default = true; break; + case CONSTR_GENERATED: + if (cxt->ofType) { + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("generated columns are not supported on typed tables"))); + } + if (saw_generated) { + ereport(ERROR, (errmodule(MOD_GEN_COL), 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->generatedCol = 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; @@ -946,6 +965,15 @@ static void transformColumnDefinition(CreateStmtContext* cxt, ColumnDef* column, transformColumnType(cxt, column); } } + + 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))); + /* * Generate ALTER FOREIGN TABLE ALTER COLUMN statement which adds * per-column foreign data wrapper options for this column. @@ -999,6 +1027,7 @@ static void transformTableConstraint(CreateStmtContext* cxt, Constraint* constra case CONSTR_NULL: case CONSTR_NOTNULL: case CONSTR_DEFAULT: + case CONSTR_GENERATED: case CONSTR_ATTR_DEFERRABLE: case CONSTR_ATTR_NOT_DEFERRABLE: case CONSTR_ATTR_DEFERRED: @@ -1417,12 +1446,26 @@ static void transformTableLikeClause( } } - if (!def->is_serial && (table_like_clause->options & CREATE_TABLE_LIKE_DEFAULTS)) { + if (!def->is_serial && (table_like_clause->options & CREATE_TABLE_LIKE_DEFAULTS) && + !GetGeneratedCol(tupleDesc, parent_attno - 1)) { /* * 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. */ def->cooked_default = this_default; + } else if (!def->is_serial && (table_like_clause->options & CREATE_TABLE_LIKE_GENERATED) && + GetGeneratedCol(tupleDesc, parent_attno - 1)) { + bool found_whole_row = false; + def->cooked_default = + map_variable_attnos(this_default, 1, 0, attmap, tupleDesc->natts, &found_whole_row); + if (found_whole_row) { + ereport(ERROR, (errmodule(MOD_GEN_COL), 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->generatedCol = GetGeneratedCol(tupleDesc, parent_attno - 1); } } @@ -2026,6 +2069,7 @@ static void transformOfType(CreateStmtContext* cxt, TypeName* ofTypename) n->colname = pstrdup(NameStr(attr->attname)); n->typname = makeTypeNameFromOid(attr->atttypid, attr->atttypmod); n->kvtype = ATT_KV_UNDEFINED; + n->generatedCol = '\0'; n->inhcount = 0; n->is_local = true; n->is_not_null = false; @@ -3782,6 +3826,7 @@ List* transformAlterTableStmt(Oid relid, AlterTableStmt* stmt, const char* query cxt.bucketOid = InvalidOid; cxt.relnodelist = NULL; cxt.toastnodelist = NULL; + cxt.ofType = false; if (RelationIsForeignTable(rel) || RelationIsStream(rel)) { cxt.canInfomationalConstraint = CAN_BUILD_INFORMATIONAL_CONSTRAINT_BY_RELID(RelationGetRelid(rel)); diff --git a/src/common/backend/utils/cache/lsyscache.cpp b/src/common/backend/utils/cache/lsyscache.cpp index 4d243483c..2b49ee743 100644 --- a/src/common/backend/utils/cache/lsyscache.cpp +++ b/src/common/backend/utils/cache/lsyscache.cpp @@ -834,6 +834,34 @@ AttrNumber get_attnum(Oid relid, const char* attname) } } +/* + * GetGenerated + * + * Given the relation id and the attribute number, + * return the "generated" field from the attrdef relation. + * + * Errors if not found. + * + * Since not generated is represented by '\0', this can also be used as a + * Boolean test. + */ +char GetGenerated(Oid relid, AttrNumber attnum) +{ + char result = '\0'; + Relation relation; + TupleDesc tupdesc; + + /* Assume we already have adequate lock */ + relation = heap_open(relid, NoLock); + + tupdesc = RelationGetDescr(relation); + result = GetGeneratedCol(tupdesc, attnum - 1); + + heap_close(relation, NoLock); + + return result; +} + /* * get_atttype * diff --git a/src/common/backend/utils/cache/partcache.cpp b/src/common/backend/utils/cache/partcache.cpp index aeea12d13..3e9c5895b 100644 --- a/src/common/backend/utils/cache/partcache.cpp +++ b/src/common/backend/utils/cache/partcache.cpp @@ -55,6 +55,7 @@ #include "optimizer/prep.h" #include "optimizer/var.h" #include "rewrite/rewriteDefine.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/smgr.h" #include "catalog/storage.h" diff --git a/src/common/backend/utils/cache/relcache.cpp b/src/common/backend/utils/cache/relcache.cpp index c97376f60..249b837e1 100644 --- a/src/common/backend/utils/cache/relcache.cpp +++ b/src/common/backend/utils/cache/relcache.cpp @@ -1381,6 +1381,7 @@ static void RelationBuildTupleDesc(Relation relation, bool onlyLoadInitDefVal) constr = (TupleConstr*)MemoryContextAllocZero(u_sess->cache_mem_cxt, sizeof(TupleConstr)); constr->has_not_null = false; + constr->has_generated_stored = false; } /* @@ -1572,10 +1573,13 @@ static void RelationBuildTupleDesc(Relation relation, bool onlyLoadInitDefVal) else constr->defval = attrdef; constr->num_defval = ndef; + constr->generatedCols = (char *)MemoryContextAllocZero(u_sess->cache_mem_cxt, + RelationGetNumberOfAttributes(relation) * sizeof(char)); AttrDefaultFetch(relation); } else { constr->num_defval = 0; constr->defval = NULL; + constr->generatedCols = NULL; } if (relation->rd_rel->relchecks > 0) /* CHECKs */ @@ -5017,28 +5021,46 @@ TupleDesc GetDefaultPgIndexDesc(void) return GetPgIndexDescriptor(); } +/* + * Load generated column attribute value definitions for the relation. + */ +static void GeneratedColFetch(TupleConstr *constr, HeapTuple htup, Relation adrel, int attrdefIndex) +{ + char *genCols = constr->generatedCols; + AttrDefault *attrdef = constr->defval; + char generatedCol = '\0'; + if (HeapTupleHeaderGetNatts(htup->t_data, adrel->rd_att) >= Anum_pg_attrdef_adgencol) { + bool isnull = false; + Datum val = fastgetattr(htup, Anum_pg_attrdef_adgencol, adrel->rd_att, &isnull); + if (!isnull) { + generatedCol = DatumGetChar(val); + } + } + attrdef[attrdefIndex].generatedCol = generatedCol; + genCols[attrdef[attrdefIndex].adnum - 1] = generatedCol; + if (generatedCol == ATTRIBUTE_GENERATED_STORED) { + constr->has_generated_stored = true; + } +} + /* * Load any default attribute value definitions for the relation. */ static void AttrDefaultFetch(Relation relation) { - AttrDefault* attrdef = relation->rd_att->constr->defval; + AttrDefault *attrdef = relation->rd_att->constr->defval; int ndef = relation->rd_att->constr->num_defval; - Relation adrel; - SysScanDesc adscan; ScanKeyData skey; HeapTuple htup; Datum val; bool isnull = false; - int found; int i; + int found = 0; - ScanKeyInit( - &skey, Anum_pg_attrdef_adrelid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(relation))); - - adrel = heap_open(AttrDefaultRelationId, AccessShareLock); - adscan = systable_beginscan(adrel, AttrDefaultIndexId, true, NULL, 1, &skey); - found = 0; + ScanKeyInit(&skey, Anum_pg_attrdef_adrelid, BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(relation))); + Relation adrel = heap_open(AttrDefaultRelationId, AccessShareLock); + SysScanDesc adscan = systable_beginscan(adrel, AttrDefaultIndexId, true, SnapshotNow, 1, &skey); while (HeapTupleIsValid(htup = systable_getnext(adscan))) { Form_pg_attrdef adform = (Form_pg_attrdef)GETSTRUCT(htup); @@ -5046,30 +5068,30 @@ static void AttrDefaultFetch(Relation relation) for (i = 0; i < ndef; i++) { if (adform->adnum != attrdef[i].adnum) continue; + if (attrdef[i].adbin != NULL) - ereport(WARNING, - (errmsg("multiple attrdef records found for attr %s of rel %s", - NameStr(relation->rd_att->attrs[adform->adnum - 1]->attname), - RelationGetRelationName(relation)))); + ereport(WARNING, (errmsg("multiple attrdef records found for attr %s of rel %s", + NameStr(relation->rd_att->attrs[adform->adnum - 1]->attname), RelationGetRelationName(relation)))); else found++; + if (t_thrd.proc->workingVersionNum >= GENERATED_COL_VERSION_NUM) { + GeneratedColFetch(relation->rd_att->constr, htup, adrel, i); + } + val = fastgetattr(htup, Anum_pg_attrdef_adbin, adrel->rd_att, &isnull); if (isnull) - ereport(WARNING, - (errmsg("null adbin for attr %s of rel %s", - NameStr(relation->rd_att->attrs[adform->adnum - 1]->attname), - RelationGetRelationName(relation)))); + ereport(WARNING, (errmsg("null adbin for attr %s of rel %s", + NameStr(relation->rd_att->attrs[adform->adnum - 1]->attname), RelationGetRelationName(relation)))); else attrdef[i].adbin = MemoryContextStrdup(u_sess->cache_mem_cxt, TextDatumGetCString(val)); break; } - if (i >= ndef) - ereport(WARNING, - (errmsg("unexpected attrdef record found for attr %d of rel %s", - adform->adnum, - RelationGetRelationName(relation)))); + if (i >= ndef) { + ereport(WARNING, (errmsg("unexpected attrdef record found for attr %d of rel %s", adform->adnum, + RelationGetRelationName(relation)))); + } } systable_endscan(adscan); diff --git a/src/common/backend/utils/error/be_module.cpp b/src/common/backend/utils/error/be_module.cpp index 74a2c7d27..5ba07ea52 100644 --- a/src/common/backend/utils/error/be_module.cpp +++ b/src/common/backend/utils/error/be_module.cpp @@ -92,6 +92,7 @@ const module_data module_map[] = {{MOD_ALL, "ALL"}, {MOD_HOTKEY, "HOTKEY"}, {MOD_THREAD_POOL, "THREAD_POOL"}, {MOD_OPT_AI, "OPT_AI"}, + {MOD_GEN_COL, "GEN_COL"}, /* add your module name above */ {MOD_MAX, "BACKEND"}}; diff --git a/src/common/backend/utils/init/globals.cpp b/src/common/backend/utils/init/globals.cpp index 0c3903acd..1400cb176 100644 --- a/src/common/backend/utils/init/globals.cpp +++ b/src/common/backend/utils/init/globals.cpp @@ -59,7 +59,7 @@ bool open_join_children = true; bool will_shutdown = false; /* hard-wired binary version number */ -const uint32 GRAND_VERSION_NUM = 92302; +const uint32 GRAND_VERSION_NUM = 92303; const uint32 MATVIEW_VERSION_NUM = 92213; const uint32 PARTIALPUSH_VERSION_NUM = 92087; @@ -77,6 +77,8 @@ const uint32 RANGE_LIST_DISTRIBUTION_VERSION_NUM = 92272; const uint32 BACKUP_SLOT_VERSION_NUM = 92282; const uint32 ML_OPT_MODEL_VERSION_NUM = 92284; const uint32 FIX_SQL_ADD_RELATION_REF_COUNT = 92291; +const uint32 GENERATED_COL_VERSION_NUM = 92303; + /* This variable indicates wheather the instance is in progress of upgrade as a whole */ uint32 volatile WorkingGrandVersionNum = GRAND_VERSION_NUM; diff --git a/src/common/interfaces/libpq/frontend_parser/gram.y b/src/common/interfaces/libpq/frontend_parser/gram.y index abaca75b4..417b640a4 100755 --- a/src/common/interfaces/libpq/frontend_parser/gram.y +++ b/src/common/interfaces/libpq/frontend_parser/gram.y @@ -521,7 +521,7 @@ extern THR_LOCAL bool stmt_contains_operator_plus; FALSE_P FAMILY FAST FENCED FETCH FILEHEADER_P FILL_MISSING_FIELDS FILTER FIRST_P FIXED_P FLOAT_P FOLLOWING FOR FORCE FOREIGN FORMATTER FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS - GLOBAL GLOBAL_FUNCTION GRANT GRANTED GREATEST GROUP_P GROUPING_P + GENERATED GLOBAL GLOBAL_FUNCTION GRANT GRANTED GREATEST GROUP_P GROUPING_P HANDLER HAVING HDFSDIRECTORY HEADER_P HOLD HOUR_P @@ -560,7 +560,7 @@ extern THR_LOCAL bool stmt_contains_operator_plus; SAVEPOINT SCHEMA SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHIPPABLE SHOW SHUTDOWN SIMILAR SIMPLE SIZE SLICE SMALLDATETIME SMALLDATETIME_FORMAT_P SMALLINT SNAPSHOT SOME SOURCE_P SPACE SPILL SPLIT STABLE STANDALONE_P START - STATEMENT STATEMENT_ID STATISTICS STDIN STDOUT STORAGE STORE_P STREAM STRICT_P STRIP_P SUBSTRING + STATEMENT STATEMENT_ID STATISTICS STDIN STDOUT STORAGE STORE_P STORED STREAM STRICT_P STRIP_P SUBSTRING SYMMETRIC SYNONYM SYSDATE SYSID SYSTEM_P SYS_REFCURSOR TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THAN THEN TIME TIME_FORMAT_P TIMESTAMP TIMESTAMP_FORMAT_P TIMESTAMPDIFF TINYINT @@ -642,7 +642,7 @@ extern THR_LOCAL bool stmt_contains_operator_plus; * blame any funny behavior of UNBOUNDED on the SQL standard, though. */ %nonassoc UNBOUNDED /* ideally should have same precedence as IDENT */ -%nonassoc IDENT NULL_P PARTITION RANGE ROWS PRECEDING FOLLOWING CUBE ROLLUP +%nonassoc IDENT GENERATED NULL_P PARTITION RANGE ROWS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %nonassoc NOTNULL %nonassoc ISNULL @@ -4904,6 +4904,20 @@ ColConstraintElem: n->cooked_expr = NULL; $$ = (Node *)n; } + | GENERATED ALWAYS AS '(' a_expr ')' STORED + { +#ifdef ENABLE_MULTIPLE_NODES + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Generated column is not yet supported."))); +#endif + Constraint *n = makeNode(Constraint); + n->contype = CONSTR_GENERATED; + n->generated_when = ATTRIBUTE_IDENTITY_ALWAYS; + 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); @@ -5044,6 +5058,7 @@ TableLikeIncludingOption: | RELOPTIONS { $$ = CREATE_TABLE_LIKE_RELOPTIONS; } | DISTRIBUTION { $$ = CREATE_TABLE_LIKE_DISTRIBUTION; } | OIDS { $$ = CREATE_TABLE_LIKE_OIDS;} + | GENERATED { $$ = CREATE_TABLE_LIKE_GENERATED; } ; TableLikeExcludingOption: @@ -5056,6 +5071,7 @@ TableLikeExcludingOption: | RELOPTIONS { $$ = CREATE_TABLE_LIKE_RELOPTIONS; } | DISTRIBUTION { $$ = CREATE_TABLE_LIKE_DISTRIBUTION; } | OIDS { $$ = CREATE_TABLE_LIKE_OIDS; } + | GENERATED { $$ = CREATE_TABLE_LIKE_GENERATED; } | ALL { $$ = CREATE_TABLE_LIKE_ALL; } ; @@ -10706,6 +10722,7 @@ unreserved_keyword: | FORCE | FORMATTER | FORWARD + | GENERATED | GLOBAL | GLOBAL_FUNCTION | GRANTED @@ -10905,6 +10922,7 @@ unreserved_keyword: | STDOUT | STORAGE | STORE_P + | STORED | STRICT_P | STRIP_P | SYNONYM diff --git a/src/common/pl/plpgsql/src/pl_exec.cpp b/src/common/pl/plpgsql/src/pl_exec.cpp index 5fcfd3360..02664f7e5 100644 --- a/src/common/pl/plpgsql/src/pl_exec.cpp +++ b/src/common/pl/plpgsql/src/pl_exec.cpp @@ -204,6 +204,7 @@ static void rebuild_exception_subtransaction_chain(PLpgSQL_execstate* estate); static void stp_check_transaction_and_set_resource_owner(ResourceOwner oldResourceOwner,TransactionId oldTransactionId); static void stp_check_transaction_and_create_econtext(PLpgSQL_execstate* estate,TransactionId oldTransactionId); +static void RecordSetGeneratedField(PLpgSQL_rec *recNew); bool plpgsql_get_current_value_stp_with_exception(); void plpgsql_restore_current_value_stp_with_exception(bool saved_current_stp_with_exception); @@ -631,6 +632,47 @@ Datum plpgsql_exec_function(PLpgSQL_function* func, FunctionCallInfo fcinfo, boo return estate.retval; } +static void RecordSetGeneratedField(PLpgSQL_rec *recNew) +{ + int natts = recNew->tupdesc->natts; + Datum* values = NULL; + bool* nulls = NULL; + bool* replaces = NULL; + HeapTuple newtup = NULL; + errno_t rc = EOK; + + /* + * Set up values/control arrays for heap_modify_tuple. For all + * the attributes except the one we want to replace, use the + * value that's in the old tuple. + */ + values = (Datum*)palloc(sizeof(Datum) * natts); + nulls = (bool*)palloc(sizeof(bool) * natts); + replaces = (bool*)palloc(sizeof(bool) * natts); + + rc = memset_s(replaces, sizeof(bool) * natts, false, sizeof(bool) * natts); + securec_check(rc, "\0", "\0"); + + for (int i = 0; i < recNew->tupdesc->natts; i++) { + if (GetGeneratedCol(recNew->tupdesc, i) == ATTRIBUTE_GENERATED_STORED) { + replaces[i] = true; + values[i] = (Datum)0; + nulls[i] = true; + } + } + newtup = heap_modify_tuple(recNew->tup, recNew->tupdesc, values, nulls, replaces); + if (recNew->freetup) { + heap_freetuple_ext(recNew->tup); + } + + recNew->tup = newtup; + recNew->freetup = true; + + pfree_ext(values); + pfree_ext(nulls); + pfree_ext(replaces); +} + /* ---------- * plpgsql_exec_trigger Called by the call handler for * trigger execution. @@ -698,6 +740,19 @@ HeapTuple plpgsql_exec_trigger(PLpgSQL_function* func, TriggerData* trigdata) } else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) { rec_new->tup = trigdata->tg_newtuple; rec_old->tup = trigdata->tg_trigtuple; + + /* + * 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 (rec_new->tupdesc->constr && rec_new->tupdesc->constr->has_generated_stored && + TRIGGER_FIRED_BEFORE(trigdata->tg_event)) { + RecordSetGeneratedField(rec_new); + } } else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) { rec_new->tup = NULL; rec_old->tup = trigdata->tg_trigtuple; diff --git a/src/common/pl/plpython/plpy_cursorobject.cpp b/src/common/pl/plpython/plpy_cursorobject.cpp index 09d1c1bb3..38d5bb5af 100644 --- a/src/common/pl/plpython/plpy_cursorobject.cpp +++ b/src/common/pl/plpython/plpy_cursorobject.cpp @@ -337,7 +337,7 @@ static PyObject* PLy_cursor_iternext(PyObject* self) PLy_input_tuple_funcs(&cursor->result, SPI_tuptable->tupdesc); } - ret = PLyDict_FromTuple(&cursor->result, SPI_tuptable->vals[0], SPI_tuptable->tupdesc); + ret = PLyDict_FromTuple(&cursor->result, SPI_tuptable->vals[0], SPI_tuptable->tupdesc, true); } SPI_freetuptable(SPI_tuptable); @@ -411,8 +411,7 @@ static PyObject* PLy_cursor_fetch(PyObject* self, PyObject* args) ret->rows = PyList_New(SPI_processed); for (uint32 i = 0; i < SPI_processed; i++) { - PyObject* row = PLyDict_FromTuple(&cursor->result, SPI_tuptable->vals[i], SPI_tuptable->tupdesc); - + PyObject *row = PLyDict_FromTuple(&cursor->result, SPI_tuptable->vals[i], SPI_tuptable->tupdesc, true); PyList_SetItem(ret->rows, i, row); } } diff --git a/src/common/pl/plpython/plpy_exec.cpp b/src/common/pl/plpython/plpy_exec.cpp index e053b5142..859c0c925 100644 --- a/src/common/pl/plpython/plpy_exec.cpp +++ b/src/common/pl/plpython/plpy_exec.cpp @@ -344,7 +344,7 @@ static PyObject* PLy_function_build_args(FunctionCallInfo fcinfo, PLyProcedure* tmptup.t_len = HeapTupleHeaderGetDatumLength(td); tmptup.t_data = td; - arg = PLyDict_FromTuple(&(proc->args[i]), &tmptup, tupdesc); + arg = PLyDict_FromTuple(&(proc->args[i]), &tmptup, tupdesc, true); ReleaseTupleDesc(tupdesc); } } else { @@ -489,7 +489,8 @@ static PyObject* PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure* p pltevent = PyString_FromString("INSERT"); PyDict_SetItemString(pltdata, "old", Py_None); - pytnew = PLyDict_FromTuple(&(proc->result), tdata->tg_trigtuple, tdata->tg_relation->rd_att); + pytnew = PLyDict_FromTuple(&(proc->result), tdata->tg_trigtuple, tdata->tg_relation->rd_att, + !TRIGGER_FIRED_BEFORE(tdata->tg_event)); PyDict_SetItemString(pltdata, "new", pytnew); Py_DECREF(pytnew); *rv = tdata->tg_trigtuple; @@ -497,17 +498,18 @@ static PyObject* PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure* p pltevent = PyString_FromString("DELETE"); PyDict_SetItemString(pltdata, "new", Py_None); - pytold = PLyDict_FromTuple(&(proc->result), tdata->tg_trigtuple, tdata->tg_relation->rd_att); + pytold = PLyDict_FromTuple(&(proc->result), tdata->tg_trigtuple, tdata->tg_relation->rd_att, true); PyDict_SetItemString(pltdata, "old", pytold); Py_DECREF(pytold); *rv = tdata->tg_trigtuple; } else if (TRIGGER_FIRED_BY_UPDATE(tdata->tg_event)) { pltevent = PyString_FromString("UPDATE"); - pytnew = PLyDict_FromTuple(&(proc->result), tdata->tg_newtuple, tdata->tg_relation->rd_att); + pytnew = PLyDict_FromTuple(&(proc->result), tdata->tg_newtuple, tdata->tg_relation->rd_att, + !TRIGGER_FIRED_BEFORE(tdata->tg_event)); PyDict_SetItemString(pltdata, "new", pytnew); Py_DECREF(pytnew); - pytold = PLyDict_FromTuple(&(proc->result), tdata->tg_trigtuple, tdata->tg_relation->rd_att); + pytold = PLyDict_FromTuple(&(proc->result), tdata->tg_trigtuple, tdata->tg_relation->rd_att, true); PyDict_SetItemString(pltdata, "old", pytold); Py_DECREF(pytold); *rv = tdata->tg_newtuple; @@ -636,6 +638,10 @@ static HeapTuple PLy_modify_tuple(PLyProcedure* proc, PyObject* pltd, TriggerDat } atti = attn - 1; + if (ISGENERATEDCOL(tupdesc, atti)) + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED), + errmsg("cannot set generated column \"%s\"", plattstr))); + plval = PyDict_GetItem(plntup, platt); if (plval == NULL) { elog(FATAL, "Python interpreter is probably corrupted"); diff --git a/src/common/pl/plpython/plpy_spi.cpp b/src/common/pl/plpython/plpy_spi.cpp index ed3841474..5fbccff34 100644 --- a/src/common/pl/plpython/plpy_spi.cpp +++ b/src/common/pl/plpython/plpy_spi.cpp @@ -397,7 +397,7 @@ static PyObject* PLy_spi_execute_fetch_result(SPITupleTable* tuptable, int rows, PLy_input_tuple_funcs(&args, tuptable->tupdesc); for (i = 0; i < rows; i++) { - PyObject* row = PLyDict_FromTuple(&args, tuptable->vals[i], tuptable->tupdesc); + PyObject *row = PLyDict_FromTuple(&args, tuptable->vals[i], tuptable->tupdesc, true); PyList_SetItem(result->rows, i, row); } diff --git a/src/common/pl/plpython/plpy_typeio.cpp b/src/common/pl/plpython/plpy_typeio.cpp index 62ca4f492..358cd2583 100644 --- a/src/common/pl/plpython/plpy_typeio.cpp +++ b/src/common/pl/plpython/plpy_typeio.cpp @@ -274,7 +274,7 @@ void PLy_output_record_funcs(PLyTypeInfo* arg, TupleDesc desc) /* * Transform a tuple into a Python dict object. */ -PyObject* PLyDict_FromTuple(PLyTypeInfo* info, HeapTuple tuple, TupleDesc desc) +PyObject *PLyDict_FromTuple(PLyTypeInfo *info, HeapTuple tuple, TupleDesc desc, bool include_generated) { PyObject* volatile dict = NULL; PLyExecutionContext* exec_ctx = PLy_current_execution_context(); @@ -307,6 +307,12 @@ PyObject* PLyDict_FromTuple(PLyTypeInfo* info, HeapTuple tuple, TupleDesc desc) continue; } + if (ISGENERATEDCOL(desc, i)) { + /* don't include unless requested */ + if (!include_generated) + continue; + } + key = NameStr(desc->attrs[i]->attname); vattr = heap_getattr(tuple, (i + 1), desc, &is_null); diff --git a/src/common/pl/plpython/plpy_typeio.h b/src/common/pl/plpython/plpy_typeio.h index 3a61d9707..6c5f2ce1e 100644 --- a/src/common/pl/plpython/plpy_typeio.h +++ b/src/common/pl/plpython/plpy_typeio.h @@ -95,6 +95,6 @@ extern void PLy_output_record_funcs(PLyTypeInfo* arg, TupleDesc desc); extern Datum PLyObject_ToCompositeDatum(PLyTypeInfo* info, TupleDesc desc, PyObject* plrv); /* conversion from heap tuples to Python dictionaries */ -extern PyObject* PLyDict_FromTuple(PLyTypeInfo* info, HeapTuple tuple, TupleDesc desc); +extern PyObject *PLyDict_FromTuple(PLyTypeInfo *info, HeapTuple tuple, TupleDesc desc, bool include_generated); #endif /* PLPY_TYPEIO_H */ diff --git a/src/gausskernel/optimizer/commands/copy.cpp b/src/gausskernel/optimizer/commands/copy.cpp index a30b0fbde..4c5c90493 100644 --- a/src/gausskernel/optimizer/commands/copy.cpp +++ b/src/gausskernel/optimizer/commands/copy.cpp @@ -4074,6 +4074,15 @@ static uint64 CopyFrom(CopyState cstate) resultRelInfo->ri_FdwRoutine->ExecForeignInsert(estate, resultRelInfo, slot, NULL); processed++; } else if (!skip_tuple) { + /* + * Compute stored generated columns + */ + if (resultRelInfo->ri_RelationDesc->rd_att->constr && + resultRelInfo->ri_RelationDesc->rd_att->constr->has_generated_stored) { + ExecComputeStoredGenerated(resultRelInfo, estate, slot, tuple, CMD_INSERT); + tuple = (HeapTuple)slot->tts_tuple; + } + /* Check the constraints of the tuple */ if (cstate->rel->rd_att->constr) ExecConstraints(resultRelInfo, slot, estate); @@ -4911,10 +4920,10 @@ CopyState BeginCopyFrom(Relation rel, const char* filename, List* attnamelist, L 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) && !ISGENERATEDCOL(tupDesc, attnum - 1)) { /* attribute is NOT to be copied from input */ /* use default value if one exists */ - Expr* defexpr = (Expr*)build_column_default(cstate->rel, attnum); + Expr *defexpr = (Expr *)build_column_default(cstate->rel, attnum); if (defexpr != NULL) { #ifdef PGXC @@ -7101,6 +7110,11 @@ List* CopyGetAllAttnums(TupleDesc tupDesc, Relation rel) * 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. */ List* CopyGetAttnums(TupleDesc tupDesc, Relation rel, List* attnamelist) @@ -7109,13 +7123,15 @@ List* CopyGetAttnums(TupleDesc tupDesc, Relation rel, List* attnamelist) if (attnamelist == NIL) { /* Generate default column list */ - Form_pg_attribute* attr = tupDesc->attrs; + Form_pg_attribute *attr = tupDesc->attrs; int attr_count = tupDesc->natts; int i; for (i = 0; i < attr_count; i++) { if (attr[i]->attisdropped) continue; + if (ISGENERATEDCOL(tupDesc, i)) + continue; attnums = lappend_int(attnums, i + 1); } } else { @@ -7133,6 +7149,11 @@ List* CopyGetAttnums(TupleDesc tupDesc, Relation rel, List* attnamelist) if (tupDesc->attrs[i]->attisdropped) continue; if (namestrcmp(&(tupDesc->attrs[i]->attname), name) == 0) { + if (ISGENERATEDCOL(tupDesc, i)) { + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("column \"%s\" is a generated column", name), + errdetail("Generated columns cannot be used in COPY."))); + } attnum = tupDesc->attrs[i]->attnum; break; } diff --git a/src/gausskernel/optimizer/commands/createas.cpp b/src/gausskernel/optimizer/commands/createas.cpp index 9b8373f02..98257c6f0 100644 --- a/src/gausskernel/optimizer/commands/createas.cpp +++ b/src/gausskernel/optimizer/commands/createas.cpp @@ -339,6 +339,7 @@ static void intorel_startup(DestReceiver* self, int operation, TupleDesc typeinf col->is_not_null = false; col->is_from_type = false; col->storage = 0; + col->generatedCol = '\0'; col->kvtype = attribute->attkvtype; col->cmprs_mode = attribute->attcmprmode; col->raw_default = NULL; diff --git a/src/gausskernel/optimizer/commands/foreigncmds.cpp b/src/gausskernel/optimizer/commands/foreigncmds.cpp index cef4e223d..d2d5a0da1 100644 --- a/src/gausskernel/optimizer/commands/foreigncmds.cpp +++ b/src/gausskernel/optimizer/commands/foreigncmds.cpp @@ -1368,6 +1368,7 @@ ColumnDef* makeColumnDef(const char* colname, char* coltype) col->colname = pstrdup(colname); col->typname = SystemTypeName(coltype); col->kvtype = 0; + col->generatedCol = '\0'; col->inhcount = 0; col->is_local = true; col->is_not_null = false; diff --git a/src/gausskernel/optimizer/commands/tablecmds.cpp b/src/gausskernel/optimizer/commands/tablecmds.cpp index cadb2aed5..6b7c8dbab 100644 --- a/src/gausskernel/optimizer/commands/tablecmds.cpp +++ b/src/gausskernel/optimizer/commands/tablecmds.cpp @@ -695,6 +695,7 @@ static bool WLMRelationCanTruncate(Relation rel); static OnCommitAction GttOncommitOption(const List *options); static void ATCheckDuplicateColumn(const AlterTableCmd* cmd, const List* tabCmds); static void ATCheckNotNullConstr(const AlterTableCmd* cmd, const AlteredTableInfo* tab); +static void DelDependencONDataType(Relation rel, Relation depRel, const Form_pg_attribute attTup); /* get all partitions oid */ static List* get_all_part_oid(Oid relid) @@ -2202,14 +2203,20 @@ Oid DefineRelation(CreateStmt* stmt, char relkind, Oid ownerId) isMOTTableFromSrvName(((CreateForeignTableStmt*)stmt)->servername) || #endif isPostgresFDWFromSrvName(((CreateForeignTableStmt*)stmt)->servername)))) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("default values on foreign tables are not supported"))); + ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("%s on foreign tables are not supported", + colDef->generatedCol ? "generated column" : "default values"))); +#ifdef ENABLE_MOT + if (IsA(stmt, CreateForeignTableStmt) && + isMOTTableFromSrvName(((CreateForeignTableStmt *)stmt)->servername) && colDef->generatedCol) { + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("generated column on foreign tables are not supported"))); + } +#endif } if (relkind == RELKIND_STREAM) { - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("default values on streams are not supported"))); + ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("%s on streams are not supported", + colDef->generatedCol ? "generated column" : "default values"))); } Assert(colDef->cooked_default == NULL); @@ -2217,13 +2224,18 @@ Oid DefineRelation(CreateStmt* stmt, char relkind, Oid ownerId) rawEnt = (RawColumnDefault*)palloc(sizeof(RawColumnDefault)); rawEnt->attnum = attnum; rawEnt->raw_default = colDef->raw_default; + rawEnt->generatedCol = colDef->generatedCol; rawDefaults = lappend(rawDefaults, rawEnt); descriptor->attrs[attnum - 1]->atthasdef = true; } else if (colDef->cooked_default != NULL) { CookedConstraint* cooked = NULL; cooked = (CookedConstraint*)palloc(sizeof(CookedConstraint)); - cooked->contype = CONSTR_DEFAULT; + + if (colDef->generatedCol) + cooked->contype = CONSTR_GENERATED; + else + cooked->contype = CONSTR_DEFAULT; cooked->name = NULL; cooked->attnum = attnum; cooked->expr = colDef->cooked_default; @@ -2417,7 +2429,7 @@ Oid DefineRelation(CreateStmt* stmt, char relkind, Oid ownerId) rel = relation_open(relationId, AccessExclusiveLock); /* - * Now add any newly specified column default values and CHECK constraints + * 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 @@ -4181,6 +4193,7 @@ static List* MergeAttributes( coldef->constraints = restdef->constraints; coldef->is_from_type = false; coldef->kvtype = restdef->kvtype; + coldef->generatedCol = restdef->generatedCol; coldef->cmprs_mode = restdef->cmprs_mode; list_delete_cell(schema, rest, prev); } else { @@ -4343,6 +4356,12 @@ static List* MergeAttributes( } /* Default and other constraints are handled below */ newattno[parent_attno - 1] = exist_attno; + + /* Check for GENERATED conflicts */ + if (def->generatedCol != GetGeneratedCol(tupleDesc, parent_attno - 1)) { + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("inherited column \"%s\" has a generation conflict", attributeName))); + } } else { /* * No, create a new inherited column @@ -4358,6 +4377,7 @@ static List* MergeAttributes( def->kvtype = attribute->attkvtype; def->cmprs_mode = attribute->attcmprmode; def->raw_default = NULL; + def->generatedCol = '\0'; def->cooked_default = NULL; def->collClause = NULL; def->collOid = attribute->attcollation; @@ -4577,11 +4597,17 @@ static List* MergeAttributes( foreach (entry, schema) { ColumnDef* def = (ColumnDef*)lfirst(entry); - if (def->cooked_default == &u_sess->cmd_cxt.bogus_marker) - ereport(ERROR, - (errcode(ERRCODE_INVALID_COLUMN_DEFINITION), + if (def->cooked_default == &u_sess->cmd_cxt.bogus_marker) { + if (def->generatedCol) { + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_INVALID_COLUMN_DEFINITION), + errmsg("column \"%s\" inherits conflicting generated column", def->colname), + errhint("To resolve the conflict, specify a generated expr explicitly."))); + } else { + ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_DEFINITION), errmsg("column \"%s\" inherits conflicting default values", def->colname), errhint("To resolve the conflict, specify a default explicitly."))); + } + } } } @@ -8684,12 +8710,12 @@ static void ATExecAddColumn(List** wqueue, AlteredTableInfo* tab, Relation rel, #else if (!isPostgresFDWFromTblOid(RelationGetRelid(rel))) { #endif - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("default values on foreign tables are not supported"))); + ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("%s on foreign tables are not supported", + colDef->generatedCol ? "generated column" : "default values"))); } } else if (relkind == RELKIND_STREAM) { - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("default values on streams are not supported"))); + ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("%s on streams are not supported", + colDef->generatedCol ? "generated column" : "default values"))); } else if (RelationIsTsStore(rel)) { ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("It's not supported to add column with default value for timeseries tables."))); @@ -8699,6 +8725,8 @@ static void ATExecAddColumn(List** wqueue, AlteredTableInfo* tab, Relation rel, rawEnt->attnum = attribute.attnum; rawEnt->raw_default = (Node*)copyObject(colDef->raw_default); + rawEnt->generatedCol = colDef->generatedCol; + /* * This function is intended for CREATE TABLE, so it processes a * _list_ of defaults, but we just do one. @@ -8788,7 +8816,7 @@ static void ATExecAddColumn(List** wqueue, AlteredTableInfo* tab, Relation rel, * also exclude temp table and column table. */ if (RelationIsCUFormat(rel) || tab->rewrite || - RelationUsesSpaceType(rel->rd_rel->relpersistence) == SP_TEMP) { + RelationUsesSpaceType(rel->rd_rel->relpersistence) == SP_TEMP || colDef->generatedCol) { ATExecAppendDefValExpr(attribute.attnum, defval, tab); } else { bytea* value = NULL; @@ -9183,6 +9211,7 @@ static void ATExecSetNotNull(AlteredTableInfo* tab, Relation rel, const char* co static void ATExecColumnDefault(Relation rel, const char* colName, Node* newDefault, LOCKMODE lockmode) { AttrNumber attnum; + TupleDesc tupdesc = RelationGetDescr(rel); /* * get the number of the attribute @@ -9206,6 +9235,11 @@ static void ATExecColumnDefault(Relation rel, const char* colName, Node* newDefa if (attnum <= 0) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot alter system column \"%s\"", colName))); + if (ISGENERATEDCOL(tupdesc, attnum - 1)) { + ereport(ERROR, (errmodule(MOD_GEN_COL), 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 @@ -9224,6 +9258,7 @@ static void ATExecColumnDefault(Relation rel, const char* colName, Node* newDefa rawEnt = (RawColumnDefault*)palloc(sizeof(RawColumnDefault)); rawEnt->attnum = attnum; rawEnt->raw_default = newDefault; + rawEnt->generatedCol = '\0'; /* * This function is intended for CREATE TABLE, so it processes a @@ -9557,6 +9592,8 @@ static bool CheckLastColumn(Relation rel, AttrNumber attrnum) for (int col = 0; col < rel->rd_att->natts; ++col) { if (rel->rd_att->attrs[col]->attisdropped) continue; + if (ISGENERATEDCOL(rel->rd_att, col)) + continue; if (col != (attrnum - 1)) { return false; } @@ -9564,6 +9601,7 @@ static bool CheckLastColumn(Relation rel, AttrNumber attrnum) return true; } + #ifdef ENABLE_MULTIPLE_NODES /* Check if the ALTER TABLE DROP COLUMN operation is valid in timeseries table. * Invalid case: @@ -10337,6 +10375,34 @@ static void ATAddForeignKeyConstraint(AlteredTableInfo* tab, Relation rel, Const checkFkeyPermissions(pkrel, pkattnum, numpks); checkFkeyPermissions(rel, fkattnum, numfks); + /* + * Check some things for generated columns. + */ + for (i = 0; i < numfks; i++) + { + char generated = GetGeneratedCol(RelationGetDescr(rel), fkattnum[i] - 1); + + if (generated) + { + /* + * 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, + (errmodule(MOD_GEN_COL), errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid %s action for foreign key constraint containing generated column", + "ON UPDATE"))); + if (fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL || + fkconstraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT) + ereport(ERROR, + (errmodule(MOD_GEN_COL), errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid %s action for foreign key constraint containing generated column", + "ON DELETE"))); + } + } + /* * Look up the equality operators to use in the constraint. * @@ -11855,6 +11921,50 @@ static bool ATColumnChangeRequiresRewrite(Node* expr, AttrNumber varattno) } } +static void DelDependencONDataType(const Relation rel, Relation depRel, const Form_pg_attribute attTup) +{ + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple depTup; + AttrNumber attnum = attTup->attnum; + + /* + * 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, possibly a collation dependency, and dependencies on + * other columns if it is a generated column. + */ + ScanKeyInit(&key[0], Anum_pg_depend_classid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(RelationRelationId)); + ScanKeyInit(&key[1], Anum_pg_depend_objid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(rel))); + ScanKeyInit(&key[2], Anum_pg_depend_objsubid, BTEqualStrategyNumber, F_INT4EQ, Int32GetDatum((int32)attnum)); + + scan = systable_beginscan(depRel, DependDependerIndexId, true, SnapshotNow, 3, key); + + while (HeapTupleIsValid(depTup = systable_getnext(scan))) { + Form_pg_depend foundDep = (Form_pg_depend)GETSTRUCT(depTup); + ObjectAddress foundObject; + foundObject.classId = foundDep->refclassid; + foundObject.objectId = foundDep->refobjid; + foundObject.objectSubId = foundDep->refobjsubid; + + if (foundDep->deptype != DEPENDENCY_NORMAL && foundDep->deptype != DEPENDENCY_AUTO) { + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("found unexpected dependency type '%c'", foundDep->deptype))); + } + if (!(foundDep->refclassid == TypeRelationId && foundDep->refobjid == attTup->atttypid) && + !(foundDep->refclassid == CollationRelationId && foundDep->refobjid == attTup->attcollation) && + !(foundDep->refclassid == RelationRelationId && foundDep->refobjid == RelationGetRelid(rel) && + foundDep->refobjsubid != 0)) { + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("found unexpected dependency for column"))); + } + + simple_heap_delete(depRel, &depTup->t_self); + } + + systable_endscan(scan); +} + static void ATExecAlterColumnType(AlteredTableInfo* tab, Relation rel, AlterTableCmd* cmd, LOCKMODE lockmode) { char* colName = cmd->name; @@ -11874,6 +11984,7 @@ static void ATExecAlterColumnType(AlteredTableInfo* tab, Relation rel, AlterTabl ScanKeyData key[3]; SysScanDesc scan; HeapTuple depTup; + char generatedCol = '\0'; attrelation = heap_open(AttributeRelationId, RowExclusiveLock); @@ -11913,6 +12024,8 @@ static void ATExecAlterColumnType(AlteredTableInfo* tab, Relation rel, AlterTabl /* And the collation */ targetcollid = GetColumnDefCollation(NULL, def, targettype); + generatedCol = GetGeneratedCol(rel->rd_att, attnum -1); + /* * If there is a default expression for the column, get it and ensure we * can coerce it to the new datatype. (We must do this before changing @@ -11937,10 +12050,17 @@ static void ATExecAlterColumnType(AlteredTableInfo* tab, Relation rel, AlterTabl COERCION_ASSIGNMENT, 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 (defaultexpr == NULL) { + if (generatedCol == ATTRIBUTE_GENERATED_STORED) { + ereport(ERROR, (errmodule(MOD_GEN_COL), 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; @@ -12011,7 +12131,18 @@ static void ATExecAlterColumnType(AlteredTableInfo* tab, Relation rel, AlterTabl * not do anything to it. */ Assert(foundObject.objectSubId == 0); - } else { + } else if (relKind == RELKIND_RELATION && foundObject.objectSubId != 0 && + GetGenerated(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, (errmodule(MOD_GEN_COL), 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)))); + }else { /* Not expecting any other direct dependencies... */ ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("unexpected object depending on column: %s", getObjectDescription(&foundObject)))); @@ -12119,31 +12250,7 @@ static void ATExecAlterColumnType(AlteredTableInfo* tab, Relation rel, AlterTabl systable_endscan(scan); - /* - * 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. - */ - ScanKeyInit(&key[0], Anum_pg_depend_classid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(RelationRelationId)); - ScanKeyInit(&key[1], Anum_pg_depend_objid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(rel))); - ScanKeyInit(&key[2], Anum_pg_depend_objsubid, BTEqualStrategyNumber, F_INT4EQ, Int32GetDatum((int32)attnum)); - - scan = systable_beginscan(depRel, DependDependerIndexId, true, SnapshotNow, 3, key); - - while (HeapTupleIsValid(depTup = systable_getnext(scan))) { - Form_pg_depend foundDep = (Form_pg_depend)GETSTRUCT(depTup); - - if (foundDep->deptype != DEPENDENCY_NORMAL) - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("found unexpected dependency type '%c'", foundDep->deptype))); - if (!(foundDep->refclassid == TypeRelationId && foundDep->refobjid == attTup->atttypid) && - !(foundDep->refclassid == CollationRelationId && foundDep->refobjid == attTup->attcollation)) - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("found unexpected dependency for column"))); - - simple_heap_delete(depRel, &depTup->t_self); - } - - systable_endscan(scan); + DelDependencONDataType(rel, depRel, attTup); heap_close(depRel, RowExclusiveLock); @@ -12199,7 +12306,7 @@ static void ATExecAlterColumnType(AlteredTableInfo* tab, Relation rel, AlterTabl */ RemoveAttrDefault(RelationGetRelid(rel), attnum, DROP_RESTRICT, true, true); - StoreAttrDefault(rel, attnum, defaultexpr); + StoreAttrDefault(rel, attnum, defaultexpr, generatedCol); } /* Cleanup */ @@ -16995,37 +17102,43 @@ List* GetPartitionkeyPos(List* partitionkeys, List* schema) { ListCell* partitionkey_cell = NULL; ListCell* schema_cell = NULL; - ColumnRef* partitionkey_ref = NULL; - ColumnDef* schema_def = NULL; int column_count = 0; - bool* is_exist = NULL; - char* partitonkey_name = NULL; List* pos = NULL; - int len = -1; if (schema == NIL) { ereport(ERROR, (errcode(ERRCODE_INVALID_OPERATION), errmsg("there is no column for a partitioned table!"))); } - len = schema->length; + int len = schema->length; if (partitionkeys == NIL) { ereport(ERROR, (errcode(ERRCODE_INVALID_OPERATION), errmsg("there is no partition key!"))); } - is_exist = (bool*)palloc(len * sizeof(bool)); + bool* is_exist = (bool*)palloc(len * sizeof(bool)); errno_t rc = EOK; rc = memset_s(is_exist, len * sizeof(bool), 0, len * sizeof(bool)); securec_check(rc, "\0", "\0"); foreach (partitionkey_cell, partitionkeys) { - partitionkey_ref = (ColumnRef*)lfirst(partitionkey_cell); - partitonkey_name = ((Value*)linitial(partitionkey_ref->fields))->val.str; + ColumnRef* partitionkey_ref = (ColumnRef*)lfirst(partitionkey_cell); + char* partitonkey_name = ((Value*)linitial(partitionkey_ref->fields))->val.str; foreach (schema_cell, schema) { - schema_def = (ColumnDef*)lfirst(schema_cell); + ColumnDef* schema_def = (ColumnDef*)lfirst(schema_cell); /* find the column that has the same name as the partitionkey */ if (!strcmp(partitonkey_name, schema_def->colname)) { + + /* + * Generated columns cannot work: They are computed after BEFORE + * triggers, but partition routing is done before all triggers. + */ + if (schema_def->generatedCol) { + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot use generated column in partition key"), + errdetail("Column \"%s\" is a generated column.", partitonkey_name))); + } + /* duplicate partitionkey name */ if (is_exist != NULL && is_exist[column_count]) { pfree_ext(is_exist); diff --git a/src/gausskernel/optimizer/commands/trigger.cpp b/src/gausskernel/optimizer/commands/trigger.cpp index c7dddfd35..42d8de690 100644 --- a/src/gausskernel/optimizer/commands/trigger.cpp +++ b/src/gausskernel/optimizer/commands/trigger.cpp @@ -77,8 +77,9 @@ * they use, so we let them be duplicated. Be sure to update all if one needs * to be changed, however. */ -#define GetUpdatedColumns(relinfo, estate) \ - (rt_fetch((relinfo)->ri_RangeTableIndex, (estate)->es_range_table)->updatedCols) +#define GET_ALL_UPDATED_COLUMNS(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); @@ -368,6 +369,19 @@ Oid CreateTrigger(CreateTrigStmt* stmt, const char* queryString, Oid relOid, Oid (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, (errmodule(MOD_GEN_COL), 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 && + ISGENERATEDCOL(RelationGetDescr(rel), var->varattno - 1)) + ereport(ERROR, (errmodule(MOD_GEN_COL), 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 */ @@ -2255,7 +2269,7 @@ void ExecBSUpdateTriggers(EState* estate, ResultRelInfo* relinfo) if (!trigdesc->trig_update_before_statement) return; - updatedCols = GetUpdatedColumns(relinfo, estate); + updatedCols = GET_ALL_UPDATED_COLUMNS(relinfo, estate); LocTriggerData.type = T_TriggerData; LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE | TRIGGER_EVENT_BEFORE; @@ -2302,7 +2316,7 @@ void ExecASUpdateTriggers(EState* estate, ResultRelInfo* relinfo) NULL, NULL, NIL, - GetUpdatedColumns(relinfo, estate)); + GET_ALL_UPDATED_COLUMNS(relinfo, estate)); } TupleTableSlot* ExecBRUpdateTriggers(EState* estate, EPQState* epqstate, ResultRelInfo* relinfo, Oid oldPartitionOid, @@ -2376,7 +2390,7 @@ TupleTableSlot* ExecBRUpdateTriggers(EState* estate, EPQState* epqstate, ResultR } #endif - updatedCols = GetUpdatedColumns(relinfo, estate); + updatedCols = GET_ALL_UPDATED_COLUMNS(relinfo, estate); LocTriggerData.type = T_TriggerData; LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE | TRIGGER_EVENT_ROW | TRIGGER_EVENT_BEFORE; @@ -2465,7 +2479,7 @@ void ExecARUpdateTriggers(EState* estate, ResultRelInfo* relinfo, Oid oldPartiti trigtuple, newtuple, recheckIndexes, - GetUpdatedColumns(relinfo, estate)); + GET_ALL_UPDATED_COLUMNS(relinfo, estate)); tableam_tops_free_tuple(trigtuple); } } diff --git a/src/gausskernel/optimizer/commands/typecmds.cpp b/src/gausskernel/optimizer/commands/typecmds.cpp index 896340d5e..f1f1623e0 100644 --- a/src/gausskernel/optimizer/commands/typecmds.cpp +++ b/src/gausskernel/optimizer/commands/typecmds.cpp @@ -854,7 +854,7 @@ void DefineDomain(CreateDomainStmt* stmt) * Cook the constr->raw_expr into an expression. Note: * name is strictly for error message */ - defaultExpr = cookDefault(pstate, constr->raw_expr, basetypeoid, basetypeMod, domainName); + defaultExpr = cookDefault(pstate, constr->raw_expr, basetypeoid, basetypeMod, domainName, 0); /* * If the expression is just a NULL constant, we treat it @@ -2015,8 +2015,8 @@ void AlterDomainDefault(List* names, Node* defaultRaw) * Cook the colDef->raw_expr into an expression. Note: Name is * strictly for error message */ - defaultExpr = cookDefault(pstate, defaultRaw, typTup->typbasetype, typTup->typtypmod, NameStr(typTup->typname)); - + defaultExpr = + cookDefault(pstate, defaultRaw, typTup->typbasetype, typTup->typtypmod, NameStr(typTup->typname), 0); /* * If the expression is just a NULL constant, we treat the command * like ALTER ... DROP DEFAULT. (But see note for same test in diff --git a/src/gausskernel/optimizer/prep/prepunion.cpp b/src/gausskernel/optimizer/prep/prepunion.cpp index 721e3fd1b..bb9af89dc 100644 --- a/src/gausskernel/optimizer/prep/prepunion.cpp +++ b/src/gausskernel/optimizer/prep/prepunion.cpp @@ -1589,6 +1589,7 @@ static void expand_inherited_rtentry(PlannerInfo* root, RangeTblEntry* rte, Inde childrte->selectedCols = translate_col_privs(rte->selectedCols, appinfo->translated_vars); childrte->insertedCols = translate_col_privs(rte->insertedCols, appinfo->translated_vars); childrte->updatedCols = translate_col_privs(rte->updatedCols, appinfo->translated_vars); + childrte->extraUpdatedCols = translate_col_privs(rte->extraUpdatedCols, appinfo->translated_vars); } /* @@ -2347,6 +2348,7 @@ void expand_internal_rtentry(PlannerInfo* root, RangeTblEntry* rte, Index rti) childrte->selectedCols = translate_col_privs(rte->selectedCols, appinfo->translated_vars); childrte->insertedCols = translate_col_privs(rte->insertedCols, appinfo->translated_vars); childrte->updatedCols = translate_col_privs(rte->updatedCols, appinfo->translated_vars); + childrte->extraUpdatedCols = translate_col_privs(rte->extraUpdatedCols, appinfo->translated_vars); } /* diff --git a/src/gausskernel/optimizer/rewrite/rewriteHandler.cpp b/src/gausskernel/optimizer/rewrite/rewriteHandler.cpp index 35f26087d..535b3bb24 100644 --- a/src/gausskernel/optimizer/rewrite/rewriteHandler.cpp +++ b/src/gausskernel/optimizer/rewrite/rewriteHandler.cpp @@ -692,6 +692,8 @@ static List* rewriteTargetListIU(List* targetList, CmdType commandType, Relation for (attrno = 1; attrno <= numattrs; attrno++) { TargetEntry* new_tle = new_tles[attrno - 1]; + bool applyDefault = false; + bool generateCol = ISGENERATEDCOL(target_relation->rd_att, attrno - 1); att_tup = target_relation->rd_att->attrs[attrno - 1]; @@ -704,8 +706,28 @@ static List* rewriteTargetListIU(List* targetList, CmdType commandType, Relation * it's an INSERT and there's no tlist entry for the column, or the * tlist entry is a DEFAULT placeholder node. */ - if ((new_tle == NULL && commandType == CMD_INSERT) || - (new_tle != NULL && new_tle->expr != NULL && IsA(new_tle->expr, SetToDefault))) { + applyDefault = ((new_tle == NULL && commandType == CMD_INSERT) || + (new_tle != NULL && new_tle->expr != NULL && IsA(new_tle->expr, SetToDefault))); + + if (generateCol && !applyDefault) { + if (commandType == CMD_INSERT && attrno_list == NULL) { + ereport(ERROR, (errmodule(MOD_GEN_COL), 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 && new_tle) { + ereport(ERROR, (errmodule(MOD_GEN_COL), 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 (generateCol) { + /* + * stored generated column will be fixed in executor + */ + new_tle = NULL; + } else if (applyDefault) { Node* new_expr = NULL; new_expr = build_column_default(target_relation, attrno, true); @@ -927,9 +949,10 @@ Node* build_column_default(Relation rel, int attrno, bool isInsertCmd) } } - if (expr == NULL) { + if (expr == NULL && !ISGENERATEDCOL(rd_att, attrno - 1)) { /* - * 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. */ expr = get_typdefault(atttype); } @@ -971,14 +994,10 @@ Node* build_column_default(Relation rel, int attrno, bool isInsertCmd) } if (expr == NULL) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("column \"%s\" is of type %s" - " but default expression is of type %s", - NameStr(att_tup->attname), - format_type_be(atttype), - format_type_be(exprtype)), - errhint("You will need to rewrite or cast the expression."))); + ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" is of type %s but %s expression is of type %s", NameStr(att_tup->attname), + format_type_be(atttype), ISGENERATEDCOL(rd_att, attrno - 1) ? "generated column" : "deault", + format_type_be(exprtype)), errhint("You will need to rewrite or cast the expression."))); /* * If there is nextval FuncExpr, we should lock the quoted sequence to avoid deadlock, * this has beed done in transformFuncExpr. See lockNextvalOnCn for more details. @@ -1042,19 +1061,26 @@ static void rewriteValuesRTE(RangeTblEntry* rte, Relation target_relation, List* forboth(lc2, sublist, lc3, attrnos) { - Node* col = (Node*)lfirst(lc2); + Node *col = (Node *)lfirst(lc2); int attrno = lfirst_int(lc3); + Form_pg_attribute att_tup = target_relation->rd_att->attrs[attrno - 1]; + bool generatedCol = ISGENERATEDCOL(target_relation->rd_att, attrno - 1); + bool applyDefault = IsA(col, SetToDefault); - if (IsA(col, SetToDefault)) { - Form_pg_attribute att_tup; - Node* new_expr = NULL; + if (generatedCol && !applyDefault) { + ereport(ERROR, (errmodule(MOD_GEN_COL), 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)))); + } - att_tup = target_relation->rd_att->attrs[attrno - 1]; + if (applyDefault) { + Node *new_expr = NULL; - if (!att_tup->attisdropped) - new_expr = build_column_default(target_relation, attrno, true); + /* stored generated column will be computed in executor */ + if (att_tup->attisdropped || generatedCol) + new_expr = NULL; else - new_expr = NULL; /* force a NULL if dropped */ + new_expr = build_column_default(target_relation, attrno, true); /* * If there is no default (ie, default is effectively NULL), @@ -1073,8 +1099,9 @@ static void rewriteValuesRTE(RangeTblEntry* rte, Relation target_relation, List* new_expr, InvalidOid, -1, att_tup->atttypid, COERCE_IMPLICIT_CAST, -1, false, false); } newList = lappend(newList, new_expr); - } else + } else { newList = lappend(newList, col); + } } newValues = lappend(newValues, newList); } @@ -1501,6 +1528,7 @@ static Query* ApplyRetrieveRule(Query* parsetree, RewriteRule* rule, int rt_inde rte->selectedCols = NULL; rte->insertedCols = NULL; rte->updatedCols = NULL; + rte->extraUpdatedCols = NULL; /* * For the most part, Vars referencing the view should remain as @@ -1565,12 +1593,14 @@ static Query* ApplyRetrieveRule(Query* parsetree, RewriteRule* rule, int rt_inde 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; /* * If FOR UPDATE/SHARE of view, mark all the contained tables as implicit @@ -1954,6 +1984,21 @@ static Query* CopyAndAddInvertedQual(Query* parsetree, Node* rule_qual, int rt_i return parsetree; } +/* + * Generated column can not be manually insert or updated. + */ +static void CheckGeneratedColConstraint(CmdType commandType, Form_pg_attribute attTup, const TargetEntry *newTle) +{ + if (commandType == CMD_INSERT) { + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot insert into column \"%s\"", NameStr(attTup->attname)), + errdetail("Column \"%s\" is a generated column.", NameStr(attTup->attname)))); + } else if (commandType == CMD_UPDATE && newTle) { + ereport(ERROR, (errmodule(MOD_GEN_COL), errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column \"%s\" can only be updated to DEFAULT", NameStr(attTup->attname)), + errdetail("Column \"%s\" is a generated column.", NameStr(attTup->attname)))); + } +} /* * very same to pg's rewriteTargetListIU. we adapt it to use for MergeInto @@ -2042,8 +2087,17 @@ static List* rewriteTargetListMergeInto( */ apply_default = ((new_tle == NULL && commandType == CMD_INSERT) || (new_tle && new_tle->expr && IsA(new_tle->expr, SetToDefault))); - - if (apply_default) { + + bool isGeneratedCol = ISGENERATEDCOL(target_relation->rd_att, attrno - 1); + if (isGeneratedCol) { + if (!apply_default) { + CheckGeneratedColConstraint(commandType, att_tup, new_tle); + } + /* + * stored generated column will be fixed in executor + */ + new_tle = NULL; + } else if (apply_default) { Node* new_expr = NULL; new_expr = build_column_default(target_relation, attrno, (commandType == CMD_INSERT)); diff --git a/src/gausskernel/runtime/executor/execMain.cpp b/src/gausskernel/runtime/executor/execMain.cpp index ad2422ea9..cdb2adb5b 100644 --- a/src/gausskernel/runtime/executor/execMain.cpp +++ b/src/gausskernel/runtime/executor/execMain.cpp @@ -147,6 +147,9 @@ extern bool anls_opt_is_on(AnalysisOpt dfx_opt); (rt_fetch((relinfo)->ri_RangeTableIndex, (estate)->es_range_table)->insertedCols) #define GetUpdatedColumns(relinfo, estate) \ (rt_fetch((relinfo)->ri_RangeTableIndex, (estate)->es_range_table)->updatedCols) +#define GET_ALL_UPDATED_COLUMNS(relinfo, estate) \ + (bms_union(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols, \ + exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->extraUpdatedCols)) /* ---------------------------------------------------------------- * report_iud_time @@ -1717,6 +1720,7 @@ void InitResultRelInfo(ResultRelInfo *resultRelInfo, Relation resultRelationDesc } resultRelInfo->ri_FdwState = NULL; resultRelInfo->ri_ConstraintExprs = NULL; + resultRelInfo->ri_GeneratedExprs = NULL; resultRelInfo->ri_junkFilter = NULL; resultRelInfo->ri_projectReturning = NULL; resultRelInfo->ri_mergeTargetRTI = 0; diff --git a/src/gausskernel/runtime/executor/nodeModifyTable.cpp b/src/gausskernel/runtime/executor/nodeModifyTable.cpp index 76cc934c7..bcda79ebd 100644 --- a/src/gausskernel/runtime/executor/nodeModifyTable.cpp +++ b/src/gausskernel/runtime/executor/nodeModifyTable.cpp @@ -55,10 +55,12 @@ #include "executor/executor.h" #include "executor/execMerge.h" #include "executor/nodeModifyTable.h" +#include "executor/tuptable.h" #include "foreign/fdwapi.h" #include "miscadmin.h" #include "nodes/execnodes.h" #include "nodes/nodeFuncs.h" +#include "rewrite/rewriteHandler.h" #ifdef PGXC #include "parser/parsetree.h" #include "pgxc/execRemote.h" @@ -85,6 +87,7 @@ #ifdef PGXC static TupleTableSlot* fill_slot_with_oldvals(TupleTableSlot* slot, HeapTupleHeader oldtuphd, Bitmapset* modifiedCols); +static void RecoredGeneratedExpr(ResultRelInfo *resultRelInfo, EState *estate, CmdType cmdtype); /* Copied from trigger.c */ #define GetUpdatedColumns(relinfo, estate) \ @@ -294,6 +297,121 @@ static void ExecCheckTIDVisible(EState* estate, Relation rel, ItemPointer tid) ReleaseBuffer(buffer); } +static void RecoredGeneratedExpr(ResultRelInfo *resultRelInfo, EState *estate, CmdType cmdtype) +{ + Relation rel = resultRelInfo->ri_RelationDesc; + TupleDesc tupdesc = RelationGetDescr(rel); + int natts = tupdesc->natts; + MemoryContext oldContext; + + 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 *)); + resultRelInfo->ri_NumGeneratedNeeded = 0; + + for (int i = 0; i < natts; i++) { + if (GetGeneratedCol(tupdesc, i) == ATTRIBUTE_GENERATED_STORED) { + Expr *expr; + + /* + * If it's an update and the current column was not marked as + * being updated, then we can skip the computation. But if + * there is a BEFORE ROW UPDATE trigger, we cannot skip + * because the trigger might affect additional columns. + */ + if (cmdtype == CMD_UPDATE && !(rel->trigdesc && rel->trigdesc->trig_update_before_row) && + !bms_is_member(i + 1 - FirstLowInvalidHeapAttributeNumber, + exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, estate)->extraUpdatedCols)) { + resultRelInfo->ri_GeneratedExprs[i] = NULL; + continue; + } + + 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); + resultRelInfo->ri_NumGeneratedNeeded++; + } + } + + (void)MemoryContextSwitchTo(oldContext); + } +} + +/* + * Compute stored generated columns for a tuple + */ +void ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo, EState *estate, TupleTableSlot *slot, HeapTuple oldtuple, + CmdType cmdtype) +{ + Relation rel = resultRelInfo->ri_RelationDesc; + TupleDesc tupdesc = RelationGetDescr(rel); + uint32 natts = (uint32)tupdesc->natts; + MemoryContext oldContext; + Datum *values; + bool *nulls; + bool *replaces; + HeapTuple newtuple; + errno_t rc = EOK; + + RecoredGeneratedExpr(resultRelInfo, estate, cmdtype); + + /* + * If no generated columns have been affected by this change, then skip + * the rest. + */ + if (resultRelInfo->ri_NumGeneratedNeeded == 0) + return; + + oldContext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + values = (Datum *)palloc(sizeof(*values) * natts); + nulls = (bool *)palloc(sizeof(*nulls) * natts); + replaces = (bool *)palloc0(sizeof(*replaces) * natts); + rc = memset_s(replaces, sizeof(bool) * natts, 0, sizeof(bool) * natts); + securec_check(rc, "\0", "\0"); + + for (uint32 i = 0; i < natts; i++) { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + + if (GetGeneratedCol(tupdesc, i) == ATTRIBUTE_GENERATED_STORED && resultRelInfo->ri_GeneratedExprs[i]) { + ExprContext *econtext; + Datum val; + bool isnull; + + econtext = GetPerTupleExprContext(estate); + econtext->ecxt_scantuple = slot; + + val = ExecEvalExpr(resultRelInfo->ri_GeneratedExprs[i], econtext, &isnull, NULL); + + /* + * We must make a copy of val as we have no guarantees about where + * memory for a pass-by-reference Datum is located. + */ + if (!isnull) + val = datumCopy(val, attr->attbyval, attr->attlen); + + values[i] = val; + nulls[i] = isnull; + replaces[i] = true; + } + } + + newtuple = heap_modify_tuple(oldtuple, tupdesc, values, nulls, replaces); + (void)ExecStoreTuple(newtuple, slot, InvalidBuffer, false); + + (void)MemoryContextSwitchTo(oldContext); +} + static bool ExecConflictUpdate(ModifyTableState* mtstate, ResultRelInfo* resultRelInfo, ItemPointer conflictTid, TupleTableSlot* planSlot, TupleTableSlot* excludedSlot, EState* estate, Relation targetRel, Oid oldPartitionOid, int2 bucketid, bool canSetTag, TupleTableSlot** returning) @@ -634,6 +752,14 @@ TupleTableSlot* ExecInsertT(ModifyTableState* state, TupleTableSlot* slot, Tuple new_id = InvalidOid; } else if (result_rel_info->ri_FdwRoutine) { + /* + * Compute stored generated columns + */ + if (result_relation_desc->rd_att->constr && result_relation_desc->rd_att->constr->has_generated_stored) { + ExecComputeStoredGenerated(result_rel_info, estate, slot, tuple, CMD_INSERT); + tuple = (HeapTuple)slot->tts_tuple; + } + #ifdef ENABLE_MOT if (result_rel_info->ri_FdwRoutine->GetFdwType && result_rel_info->ri_FdwRoutine->GetFdwType() == MOT_ORC) { if (result_relation_desc->rd_att->constr) { @@ -660,6 +786,13 @@ TupleTableSlot* ExecInsertT(ModifyTableState* state, TupleTableSlot* slot, Tuple new_id = InvalidOid; } else { + /* + * Compute stored generated columns + */ + if (result_relation_desc->rd_att->constr && result_relation_desc->rd_att->constr->has_generated_stored) { + ExecComputeStoredGenerated(result_rel_info, estate, slot, tuple, CMD_INSERT); + tuple = (HeapTuple)slot->tts_tuple; + } /* * Check the constraints of the tuple */ @@ -1355,6 +1488,14 @@ TupleTableSlot* ExecUpdate(ItemPointer tupleid, /* trigger might have changed tuple */ tuple = ExecMaterializeSlot(slot); } else if (result_rel_info->ri_FdwRoutine) { + /* + * Compute stored generated columns + */ + if (result_relation_desc->rd_att->constr && result_relation_desc->rd_att->constr->has_generated_stored) { + ExecComputeStoredGenerated(result_rel_info, estate, slot, tuple, CMD_UPDATE); + tuple = (HeapTuple)slot->tts_tuple; + } + /* * update in foreign table: let the FDW do it */ @@ -1381,6 +1522,14 @@ TupleTableSlot* ExecUpdate(ItemPointer tupleid, } else { bool update_indexes = false; + /* + * Compute stored generated columns + */ + if (result_relation_desc->rd_att->constr && result_relation_desc->rd_att->constr->has_generated_stored) { + ExecComputeStoredGenerated(result_rel_info, estate, slot, tuple, CMD_UPDATE); + tuple = (HeapTuple)slot->tts_tuple; + } + /* * Check the constraints of the tuple * diff --git a/src/gausskernel/runtime/executor/opfusion.cpp b/src/gausskernel/runtime/executor/opfusion.cpp index 28b33dd6a..edc519dcf 100644 --- a/src/gausskernel/runtime/executor/opfusion.cpp +++ b/src/gausskernel/runtime/executor/opfusion.cpp @@ -35,6 +35,7 @@ #include "catalog/heap.h" #include "commands/copy.h" #include "executor/nodeIndexscan.h" +#include "executor/nodeModifyTable.h" #include "gstrace/executer_gstrace.h" #include "instruments/instr_unique_sql.h" #include "libpq/pqformat.h" @@ -1593,6 +1594,18 @@ bool InsertFusion::execute(long max_rows, char* completionTag) (void)ExecStoreTuple(tuple, m_local.m_reslot, InvalidBuffer, false); + /* + * Compute stored generated columns + */ + if (result_rel_info->ri_RelationDesc->rd_att->constr && + result_rel_info->ri_RelationDesc->rd_att->constr->has_generated_stored) { + ExecComputeStoredGenerated(result_rel_info, m_c_local.m_estate, m_local.m_reslot, tuple, CMD_INSERT); + if (tuple != (HeapTuple)m_local.m_reslot->tts_tuple) { + tableam_tops_free_tuple(tuple); + tuple = (HeapTuple)m_local.m_reslot->tts_tuple; + } + } + if (rel->rd_att->constr) { ExecConstraints(result_rel_info, m_local.m_reslot, m_c_local.m_estate); } @@ -1996,6 +2009,16 @@ bool UpdateFusion::execute(long max_rows, char* completionTag) } (void)ExecStoreTuple(tup, m_local.m_reslot, InvalidBuffer, false); + + if (result_rel_info->ri_RelationDesc->rd_att->constr && + result_rel_info->ri_RelationDesc->rd_att->constr->has_generated_stored) { + ExecComputeStoredGenerated(result_rel_info, m_c_local.m_estate, m_local.m_reslot, tup, CMD_UPDATE); + if (tup != (HeapTuple)m_local.m_reslot->tts_tuple) { + tableam_tops_free_tuple(tup); + tup = (HeapTuple)m_local.m_reslot->tts_tuple; + } + } + if (rel->rd_att->constr) ExecConstraints(result_rel_info, m_local.m_reslot, m_c_local.m_estate); diff --git a/src/gausskernel/storage/access/common/tupdesc.cpp b/src/gausskernel/storage/access/common/tupdesc.cpp index c4e4d581e..fd3fab109 100644 --- a/src/gausskernel/storage/access/common/tupdesc.cpp +++ b/src/gausskernel/storage/access/common/tupdesc.cpp @@ -191,6 +191,54 @@ TupleDesc CreateTupleDescCopy(TupleDesc tupdesc) return desc; } +/* + * TupleConstrCopy + * This function creates a new TupleConstr by copying from an existing TupleConstr. + */ +static TupleConstr *TupleConstrCopy(const TupleDesc tupdesc) +{ + TupleConstr *constr = tupdesc->constr; + TupleConstr *cpy = (TupleConstr *)palloc0(sizeof(TupleConstr)); + + cpy->has_not_null = constr->has_not_null; + cpy->has_generated_stored = constr->has_generated_stored; + + errno_t rc = EOK; + if ((cpy->num_defval = constr->num_defval) > 0) { + cpy->defval = (AttrDefault *)palloc(cpy->num_defval * sizeof(AttrDefault)); + rc = memcpy_s(cpy->defval, cpy->num_defval * sizeof(AttrDefault), constr->defval, + cpy->num_defval * sizeof(AttrDefault)); + securec_check(rc, "\0", "\0"); + for (int i = cpy->num_defval - 1; i >= 0; i--) { + if (constr->defval[i].adbin) { + cpy->defval[i].adbin = pstrdup(constr->defval[i].adbin); + } + } + uint32 genColsLen = (uint32)tupdesc->natts * sizeof(char); + cpy->generatedCols = (char *)palloc(genColsLen); + rc = memcpy_s(cpy->generatedCols, genColsLen, constr->generatedCols, genColsLen); + securec_check(rc, "\0", "\0"); + } + + if ((cpy->num_check = constr->num_check) > 0) { + cpy->check = (ConstrCheck *)palloc(cpy->num_check * sizeof(ConstrCheck)); + rc = memcpy_s(cpy->check, cpy->num_check * sizeof(ConstrCheck), constr->check, + cpy->num_check * sizeof(ConstrCheck)); + securec_check(rc, "\0", "\0"); + for (int i = cpy->num_check - 1; i >= 0; i--) { + if (constr->check[i].ccname) { + cpy->check[i].ccname = pstrdup(constr->check[i].ccname); + } + if (constr->check[i].ccbin) { + cpy->check[i].ccbin = pstrdup(constr->check[i].ccbin); + } + cpy->check[i].ccvalid = constr->check[i].ccvalid; + cpy->check[i].ccnoinherit = constr->check[i].ccnoinherit; + } + } + return cpy; +} + /* * CreateTupleDescCopyConstr * This function creates a new TupleDesc by copying from an existing @@ -198,53 +246,16 @@ TupleDesc CreateTupleDescCopy(TupleDesc tupdesc) */ TupleDesc CreateTupleDescCopyConstr(TupleDesc tupdesc) { - TupleDesc desc; - TupleConstr *constr = tupdesc->constr; - int i; errno_t rc = EOK; + TupleDesc desc = CreateTemplateTupleDesc(tupdesc->natts, tupdesc->tdhasoid, tupdesc->tdTableAmType); - desc = CreateTemplateTupleDesc(tupdesc->natts, tupdesc->tdhasoid, tupdesc->tdTableAmType); - - for (i = 0; i < desc->natts; i++) { + for (int i = 0; i < desc->natts; i++) { rc = memcpy_s(desc->attrs[i], ATTRIBUTE_FIXED_PART_SIZE, tupdesc->attrs[i], ATTRIBUTE_FIXED_PART_SIZE); securec_check(rc, "\0", "\0"); } - if (constr != NULL) { - TupleConstr *cpy = (TupleConstr *)palloc0(sizeof(TupleConstr)); - - cpy->has_not_null = constr->has_not_null; - - if ((cpy->num_defval = constr->num_defval) > 0) { - cpy->defval = (AttrDefault *)palloc(cpy->num_defval * sizeof(AttrDefault)); - rc = memcpy_s(cpy->defval, cpy->num_defval * sizeof(AttrDefault), constr->defval, - cpy->num_defval * sizeof(AttrDefault)); - securec_check(rc, "\0", "\0"); - for (i = cpy->num_defval - 1; i >= 0; i--) { - if (constr->defval[i].adbin) { - cpy->defval[i].adbin = pstrdup(constr->defval[i].adbin); - } - } - } - - if ((cpy->num_check = constr->num_check) > 0) { - cpy->check = (ConstrCheck *)palloc(cpy->num_check * sizeof(ConstrCheck)); - rc = memcpy_s(cpy->check, cpy->num_check * sizeof(ConstrCheck), constr->check, - cpy->num_check * sizeof(ConstrCheck)); - securec_check(rc, "\0", "\0"); - for (i = cpy->num_check - 1; i >= 0; i--) { - if (constr->check[i].ccname) { - cpy->check[i].ccname = pstrdup(constr->check[i].ccname); - } - if (constr->check[i].ccbin) { - cpy->check[i].ccbin = pstrdup(constr->check[i].ccbin); - } - cpy->check[i].ccvalid = constr->check[i].ccvalid; - cpy->check[i].ccnoinherit = constr->check[i].ccnoinherit; - } - } - - desc->constr = cpy; + if (tupdesc->constr != NULL) { + desc->constr = TupleConstrCopy(tupdesc); } /* copy the attinitdefval */ @@ -259,6 +270,17 @@ TupleDesc CreateTupleDescCopyConstr(TupleDesc tupdesc) return desc; } +char GetGeneratedCol(TupleDesc tupdesc, int atti) +{ + if (!tupdesc->constr) + return '\0'; + + if (tupdesc->constr->num_defval == 0) + return '\0'; + + return tupdesc->constr->generatedCols[atti]; +} + /* * Free a TupleDesc including all substructure */ @@ -281,6 +303,7 @@ void FreeTupleDesc(TupleDesc tupdesc) pfree(attrdef[i].adbin); } pfree(attrdef); + pfree(tupdesc->constr->generatedCols); } if (tupdesc->constr->num_check > 0) { ConstrCheck *check = tupdesc->constr->check; @@ -542,6 +565,10 @@ bool equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2) return false; } + if (constr1->has_generated_stored != constr2->has_generated_stored) { + return false; + } + /* check whether default values are eqaul to. */ n = constr1->num_defval; if (n != (int)constr2->num_defval) { @@ -567,6 +594,9 @@ bool equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2) if (strcmp(defval1->adbin, defval2->adbin) != 0) { return false; } + if (defval1->generatedCol != defval2->generatedCol) { + return false; + } } /* check whether check constraints are equal to. */ @@ -606,6 +636,60 @@ bool equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2) return compareInitdefvals(tupdesc1, tupdesc2); } +static bool ComparePgAttribute(Form_pg_attribute attr1, Form_pg_attribute attr2) +{ + /* + * We do not need to check every single field here: we can disregard + * attrelid and attnum (which were used to place the row in the attrs + * array in the first place). It might look like we could dispense + * with checking attlen/attbyval/attalign, since these are derived + * from atttypid; but in the case of dropped columns we must check + * them (since atttypid will be zero for all dropped columns) and in + * general it seems safer to check them always. + * + * attcacheoff must NOT be checked since it's possibly not set in both + * copies. + */ + if (strcmp(NameStr(attr1->attname), NameStr(attr2->attname)) != 0) + return false; + if (attr1->atttypid != attr2->atttypid) + return false; + if (attr1->attstattarget != attr2->attstattarget) + return false; + if (attr1->attlen != attr2->attlen) + return false; + if (attr1->attndims != attr2->attndims) + return false; + if (attr1->atttypmod != attr2->atttypmod) + return false; + if (attr1->attbyval != attr2->attbyval) + return false; + if (attr1->attstorage != attr2->attstorage) + return false; + if (attr1->attkvtype != attr2->attkvtype) + return false; + if (attr1->attcmprmode != attr2->attcmprmode) + return false; + if (attr1->attalign != attr2->attalign) + return false; + if (attr1->attnotnull != attr2->attnotnull) + return false; + if (attr1->atthasdef != attr2->atthasdef) + return false; + + if (attr1->attisdropped != attr2->attisdropped) + return false; + if (attr1->attislocal != attr2->attislocal) + return false; + if (attr1->attinhcount != attr2->attinhcount) + return false; + if (attr1->attcollation != attr2->attcollation) + return false; + /* attacl, attoptions and attfdwoptions are not even present... */ + + return true; +} + /* * Compare the delta TupleDesc structures with column store main TupleDesc * @@ -622,53 +706,13 @@ bool equalDeltaTupleDescs(TupleDesc mainTupdesc, TupleDesc deltaTupdesc) Form_pg_attribute attr1 = mainTupdesc->attrs[i]; Form_pg_attribute attr2 = deltaTupdesc->attrs[i]; - /* - * We do not need to check every single field here: we can disregard - * attrelid and attnum (which were used to place the row in the attrs - * array in the first place). It might look like we could dispense - * with checking attlen/attbyval/attalign, since these are derived - * from atttypid; but in the case of dropped columns we must check - * them (since atttypid will be zero for all dropped columns) and in - * general it seems safer to check them always. - * - * attcacheoff must NOT be checked since it's possibly not set in both - * copies. - */ - if (strcmp(NameStr(attr1->attname), NameStr(attr2->attname)) != 0) + if (GetGeneratedCol(mainTupdesc, i) != GetGeneratedCol(deltaTupdesc, i)) { return false; - if (attr1->atttypid != attr2->atttypid) + } + + if (!ComparePgAttribute(attr1, attr2)) { return false; - if (attr1->attstattarget != attr2->attstattarget) - return false; - if (attr1->attlen != attr2->attlen) - return false; - if (attr1->attndims != attr2->attndims) - return false; - if (attr1->atttypmod != attr2->atttypmod) - return false; - if (attr1->attbyval != attr2->attbyval) - return false; - if (attr1->attstorage != attr2->attstorage) - return false; - if (attr1->attkvtype != attr2->attkvtype) - return false; - if (attr1->attcmprmode != attr2->attcmprmode) - return false; - if (attr1->attalign != attr2->attalign) - return false; - if (attr1->attnotnull != attr2->attnotnull) - return false; - if (attr1->atthasdef != attr2->atthasdef) - return false; - if (attr1->attisdropped != attr2->attisdropped) - return false; - if (attr1->attislocal != attr2->attislocal) - return false; - if (attr1->attinhcount != attr2->attinhcount) - return false; - if (attr1->attcollation != attr2->attcollation) - return false; - /* attacl, attoptions and attfdwoptions are not even present... */ + } } /* compare the attinitdefval */ @@ -993,6 +1037,7 @@ TupleDesc BuildDescForRelation(List *schema, Node *orientedFrom, char relkind) constr->num_defval = 0; constr->check = NULL; constr->num_check = 0; + constr->generatedCols = NULL; desc->constr = constr; } else { desc->constr = NULL; diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h index 4c40cd14e..8f1dab5cf 100644 --- a/src/include/access/tupdesc.h +++ b/src/include/access/tupdesc.h @@ -49,6 +49,7 @@ const static uint32 UHEAP_TUPLE = 2; typedef struct attrDefault { AttrNumber adnum; char* adbin; /* nodeToString representation of expr */ + char generatedCol; /* generated column setting */ } AttrDefault; typedef struct constrCheck { @@ -67,6 +68,8 @@ typedef struct tupleConstr { uint16 num_defval; uint16 num_check; bool has_not_null; + bool has_generated_stored; + char* generatedCols; /* attribute array */ } TupleConstr; /* This structure contains initdefval of a tuple */ @@ -136,6 +139,8 @@ typedef struct tupleDesc { /* Accessor for the i'th attribute of tupdesc. */ #define TupleDescAttr(tupdesc, i) (tupdesc->attrs[(i)]) +#define ISGENERATEDCOL(tupdesc, i) \ + ((tupdesc)->constr != NULL && (tupdesc)->constr->num_defval > 0 && (tupdesc)->constr->generatedCols[(i)]) extern TupleDesc CreateTemplateTupleDesc(int natts, bool hasoid, TableAmType tam = TAM_HEAP); @@ -180,4 +185,6 @@ extern bool tupledesc_have_pck(TupleConstr* constr); extern void copyDroppedAttribute(Form_pg_attribute target, Form_pg_attribute source); +extern char GetGeneratedCol(TupleDesc tupdesc, int atti); + #endif /* TUPDESC_H */ diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h index a820f575e..37f0ca0b9 100644 --- a/src/include/catalog/heap.h +++ b/src/include/catalog/heap.h @@ -27,6 +27,7 @@ typedef struct RawColumnDefault { AttrNumber attnum; /* attribute to attach default to */ Node *raw_default; /* default value (untransformed parse tree) */ + char generatedCol; /* generated column setting */ } RawColumnDefault; typedef struct CookedConstraint { @@ -160,8 +161,9 @@ extern void InsertPgClassTuple(Relation pg_class_desc, Relation new_rel_desc, Oi extern List *AddRelationNewConstraints(Relation rel, List *newColDefaults, List *newConstraints, bool allow_merge, bool is_local); extern List *AddRelClusterConstraints(Relation rel, List *clusterKeys); -extern void StoreAttrDefault(Relation rel, AttrNumber attnum, Node *expr); -extern Node *cookDefault(ParseState *pstate, Node *raw_default, Oid atttypid, int32 atttypmod, char *attname); +extern void StoreAttrDefault(Relation rel, AttrNumber attnum, Node *expr, char generatedCol); +extern Node *cookDefault(ParseState *pstate, Node *raw_default, Oid atttypid, int32 atttypmod, char *attname, + char generatedCol); extern void DeleteRelationTuple(Oid relid); extern void DeleteAttributeTuples(Oid relid); extern void DeleteSystemAttributeTuples(Oid relid); diff --git a/src/include/catalog/pg_attrdef.h b/src/include/catalog/pg_attrdef.h index e9c201886..e800ca6e3 100644 --- a/src/include/catalog/pg_attrdef.h +++ b/src/include/catalog/pg_attrdef.h @@ -38,6 +38,7 @@ CATALOG(pg_attrdef,2604) BKI_SCHEMA_MACRO pg_node_tree adbin; /* nodeToString representation of default */ text adsrc; /* human-readable representation of default */ #endif + char adgencol; /* generated column setting */ } FormData_pg_attrdef; /* ---------------- @@ -51,10 +52,11 @@ typedef FormData_pg_attrdef *Form_pg_attrdef; * compiler constants for pg_attrdef * ---------------- */ -#define Natts_pg_attrdef 4 +#define Natts_pg_attrdef 5 #define Anum_pg_attrdef_adrelid 1 #define Anum_pg_attrdef_adnum 2 #define Anum_pg_attrdef_adbin 3 #define Anum_pg_attrdef_adsrc 4 +#define Anum_pg_attrdef_adgencol 5 #endif /* PG_ATTRDEF_H */ diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h index 20f5cdd55..0684bb12f 100644 --- a/src/include/catalog/pg_attribute.h +++ b/src/include/catalog/pg_attribute.h @@ -231,5 +231,7 @@ typedef FormData_pg_attribute *Form_pg_attribute; * genbki.pl. Only "bootstrapped" relations need be included. * ---------------- */ +#define ATTRIBUTE_IDENTITY_ALWAYS 'a' +#define ATTRIBUTE_GENERATED_STORED 's' #endif /* PG_ATTRIBUTE_H */ diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 1685e06c2..edd52e1a3 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -367,6 +367,11 @@ extern bool ExecRelationIsTargetRelation(EState* estate, Index scanrelid); extern Relation ExecOpenScanRelation(EState* estate, Index scanrelid); extern void ExecCloseScanRelation(Relation scanrel); +static inline RangeTblEntry *exec_rt_fetch(Index rti, EState *estate) +{ + return (RangeTblEntry *)list_nth(estate->es_range_table, rti - 1); +} + extern Partition ExecOpenScanParitition( EState* estate, Relation parent, PartitionIdentifier* partID, LOCKMODE lockmode); diff --git a/src/include/executor/nodeModifyTable.h b/src/include/executor/nodeModifyTable.h index 415060c53..b248e4055 100644 --- a/src/include/executor/nodeModifyTable.h +++ b/src/include/executor/nodeModifyTable.h @@ -61,4 +61,7 @@ ExecHBucketInsertT(ModifyTableState* state, TupleTableSlot *slot, extern void ExecCheckPlanOutput(Relation resultRel, List* targetList); +extern void ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo, EState *estate, TupleTableSlot *slot, + HeapTuple oldtuple, CmdType cmdtype); + #endif /* NODEMODIFYTABLE_H */ diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index ad912113b..c42ec48fe 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -53,6 +53,7 @@ extern const uint32 PRIVS_VERSION_NUM; extern const uint32 ML_OPT_MODEL_VERSION_NUM; extern const uint32 RANGE_LIST_DISTRIBUTION_VERSION_NUM; extern const uint32 FIX_SQL_ADD_RELATION_REF_COUNT; +extern const uint32 GENERATED_COL_VERSION_NUM; #define INPLACE_UPGRADE_PRECOMMIT_VERSION 1 diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index e0b3eb751..684962289 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -383,6 +383,53 @@ typedef struct MergeState { List* notMatchedActionStates; } MergeState; +/* ---------------------------------------------------------------- + * Expression State Trees + * + * Each executable expression tree has a parallel ExprState tree. + * + * Unlike PlanState, there is not an exact one-for-one correspondence between + * ExprState node types and Expr node types. Many Expr node types have no + * need for node-type-specific run-time state, and so they can use plain + * ExprState or GenericExprState as their associated ExprState node type. + * ---------------------------------------------------------------- + */ + +/* ---------------- + * ExprState node + * + * ExprState is the common superclass for all ExprState-type nodes. + * + * It can also be instantiated directly for leaf Expr nodes that need no + * local run-time state (such as Var, Const, or Param). + * + * To save on dispatch overhead, each ExprState node contains a function + * pointer to the routine to execute to evaluate the node. + * ---------------- + */ +typedef struct ExprState ExprState; + +typedef Datum (*ExprStateEvalFunc)(ExprState* expression, ExprContext* econtext, bool* isNull, ExprDoneCond* isDone); +typedef ScalarVector* (*VectorExprFun)( + ExprState* expression, ExprContext* econtext, bool* selVector, ScalarVector* inputVector, ExprDoneCond* isDone); + +typedef void* (*exprFakeCodeGenSig)(void*); +struct ExprState { + NodeTag type; + Expr* expr; /* associated Expr node */ + ExprStateEvalFunc evalfunc; /* routine to run to execute node */ + + // vectorized evaluator + // + VectorExprFun vecExprFun; + + exprFakeCodeGenSig exprCodeGen; /* routine to run llvm assembler function */ + + ScalarVector tmpVector; + + Oid resultType; +}; + /* ---------------- * ResultRelInfo information * @@ -447,6 +494,12 @@ typedef struct ResultRelInfo { */ Index ri_mergeTargetRTI; ProjectionInfo* ri_updateProj; + + /* array of stored generated columns expr states */ + ExprState **ri_GeneratedExprs; + + /* number of stored generated columns we need to compute */ + int ri_NumGeneratedNeeded; } ResultRelInfo; /* bloom filter controller */ @@ -680,53 +733,6 @@ typedef HASH_SEQ_STATUS TupleHashIterator; } while (0) #define ScanTupleHashTable(iter) ((TupleHashEntry)hash_seq_search(iter)) -/* ---------------------------------------------------------------- - * Expression State Trees - * - * Each executable expression tree has a parallel ExprState tree. - * - * Unlike PlanState, there is not an exact one-for-one correspondence between - * ExprState node types and Expr node types. Many Expr node types have no - * need for node-type-specific run-time state, and so they can use plain - * ExprState or GenericExprState as their associated ExprState node type. - * ---------------------------------------------------------------- - */ - -/* ---------------- - * ExprState node - * - * ExprState is the common superclass for all ExprState-type nodes. - * - * It can also be instantiated directly for leaf Expr nodes that need no - * local run-time state (such as Var, Const, or Param). - * - * To save on dispatch overhead, each ExprState node contains a function - * pointer to the routine to execute to evaluate the node. - * ---------------- - */ -typedef struct ExprState ExprState; - -typedef Datum (*ExprStateEvalFunc)(ExprState* expression, ExprContext* econtext, bool* isNull, ExprDoneCond* isDone); -typedef ScalarVector* (*VectorExprFun)( - ExprState* expression, ExprContext* econtext, bool* selVector, ScalarVector* inputVector, ExprDoneCond* isDone); - -typedef void* (*exprFakeCodeGenSig)(void*); -struct ExprState { - NodeTag type; - Expr* expr; /* associated Expr node */ - ExprStateEvalFunc evalfunc; /* routine to run to execute node */ - - // vectorized evaluator - // - VectorExprFun vecExprFun; - - exprFakeCodeGenSig exprCodeGen; /* routine to run llvm assembler function */ - - ScalarVector tmpVector; - - Oid resultType; -}; - /* ---------------- * GenericExprState node * diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 22b7afdd5..919a28d3a 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -342,6 +342,7 @@ typedef struct RangeTblEntry { bool isexcluded; /* the rel is the EXCLUDED relation for UPSERT */ /* For sublink in targetlist pull up */ bool sublink_pull_up; /* mark the subquery is sublink pulled up */ + Bitmapset *extraUpdatedCols; /* generated columns being updated */ } RangeTblEntry; /* @@ -751,6 +752,9 @@ typedef struct RangePartitionindexDefState { case CONSTR_ATTR_IMMEDIATE: \ tname = "ATTR IMMEDIATE"; \ break; \ + case CONSTR_GENERATED: \ + tname = "GENERATED COL"; \ + break; \ } \ tname; \ }) diff --git a/src/include/nodes/parsenodes_common.h b/src/include/nodes/parsenodes_common.h index 2c8f7b482..0af2d9f98 100644 --- a/src/include/nodes/parsenodes_common.h +++ b/src/include/nodes/parsenodes_common.h @@ -860,6 +860,7 @@ typedef struct ColumnDef { ClientLogicColumnRef *clientLogicColumnRef; Position *position; Form_pg_attribute dropped_attr; /* strcuture for dropped attribute during create table like OE */ + char generatedCol; /* generated column setting */ } ColumnDef; /* @@ -1068,7 +1069,8 @@ typedef enum ConstrType { /* types of constraints */ CONSTR_ATTR_DEFERRABLE, /* attributes for previous constraint node */ CONSTR_ATTR_NOT_DEFERRABLE, CONSTR_ATTR_DEFERRED, - CONSTR_ATTR_IMMEDIATE + CONSTR_ATTR_IMMEDIATE, + CONSTR_GENERATED } ConstrType; typedef struct Constraint { @@ -1120,6 +1122,8 @@ typedef struct Constraint { * Field used for soft constraint, which works on HDFS foreign table. */ InformationalConstraint *inforConstraint; + char generated_when; /* ALWAYS or BY DEFAULT */ + char generated_kind; /* currently always STORED */ } Constraint; /* @@ -1131,7 +1135,7 @@ typedef struct TableLikeClause { bits32 options; /* OR of TableLikeOption flags */ } TableLikeClause; -#define MAX_TABLE_LIKE_OPTIONS (10) +#define MAX_TABLE_LIKE_OPTIONS (11) typedef enum TableLikeOption { CREATE_TABLE_LIKE_DEFAULTS = 1 << 0, CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 1, @@ -1143,6 +1147,7 @@ typedef enum TableLikeOption { CREATE_TABLE_LIKE_DISTRIBUTION = 1 << 7, CREATE_TABLE_LIKE_OIDS = 1 << 8, CREATE_TABLE_LIKE_DEFAULTS_SERIAL = 1 << 9, /* Backward compatibility. Inherits serial defaults by default. */ + CREATE_TABLE_LIKE_GENERATED = 1 << 10, CREATE_TABLE_LIKE_ALL = 0x7FFFFFFF } TableLikeOption; diff --git a/src/include/optimizer/plancat.h b/src/include/optimizer/plancat.h index 6ecddd8ac..a1097aada 100644 --- a/src/include/optimizer/plancat.h +++ b/src/include/optimizer/plancat.h @@ -48,4 +48,6 @@ extern Selectivity join_selectivity( extern void estimatePartitionSize( Relation relation, Oid partitionid, int32* attr_widths, RelPageType* pages, double* tuples, double* allvisfrac); +extern bool HasStoredGeneratedColumns(const PlannerInfo *root, Index rti); + #endif /* PLANCAT_H */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 26c558b9c..39733cd56 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -245,6 +245,7 @@ PG_KEYWORD("from", FROM, RESERVED_KEYWORD) PG_KEYWORD("full", FULL, TYPE_FUNC_NAME_KEYWORD) PG_KEYWORD("function", FUNCTION, UNRESERVED_KEYWORD) PG_KEYWORD("functions", FUNCTIONS, UNRESERVED_KEYWORD) +PG_KEYWORD("generated", GENERATED, UNRESERVED_KEYWORD) PG_KEYWORD("global", GLOBAL, UNRESERVED_KEYWORD) PG_KEYWORD("global_function", GLOBAL_FUNCTION, UNRESERVED_KEYWORD) PG_KEYWORD("grant", GRANT, RESERVED_KEYWORD) @@ -526,6 +527,7 @@ PG_KEYWORD("stdin", STDIN, UNRESERVED_KEYWORD) PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD) PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD) PG_KEYWORD("store", STORE_P, UNRESERVED_KEYWORD) +PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD) PG_KEYWORD("stream", STREAM, UNRESERVED_KEYWORD) PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD) PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD) diff --git a/src/include/parser/parse_merge.h b/src/include/parser/parse_merge.h index 6074e1dac..b78421927 100644 --- a/src/include/parser/parse_merge.h +++ b/src/include/parser/parse_merge.h @@ -20,4 +20,5 @@ extern List* expandTargetTL(List* te_list, Query* parsetree); extern List* expandActionTL(List* te_list, Query* parsetree); extern List* expandQualTL(List* te_list, Query* parsetree); extern bool check_unique_constraint(List*& index_list); +extern void setExtraUpdatedCols(ParseState* pstate); #endif diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index 3d0601e60..966dda587 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -23,6 +23,62 @@ typedef struct PlusJoinRTEInfo { List* info; /* List of PlusJoinRTEItem we record */ } PlusJoinRTEInfo; +/* + * Expression kinds distinguished by transformExpr(). Many of these are not + * semantically distinct so far as expression transformation goes; rather, + * we distinguish them so that context-specific error messages can be printed. + * + * Note: EXPR_KIND_OTHER is not used in the core code, but is left for use + * by extension code that might need to call transformExpr(). The core code + * will not enforce any context-driven restrictions on EXPR_KIND_OTHER + * expressions, so the caller would have to check for sub-selects, aggregates, + * window functions, SRFs, etc if those need to be disallowed. + */ +typedef enum ParseExprKind +{ + EXPR_KIND_NONE = 0, /* "not in an expression" */ + EXPR_KIND_OTHER, /* reserved for extensions */ + EXPR_KIND_JOIN_ON, /* JOIN ON */ + EXPR_KIND_JOIN_USING, /* JOIN USING */ + EXPR_KIND_FROM_SUBSELECT, /* sub-SELECT in FROM clause */ + EXPR_KIND_FROM_FUNCTION, /* function in FROM clause */ + EXPR_KIND_WHERE, /* WHERE */ + EXPR_KIND_HAVING, /* HAVING */ + EXPR_KIND_FILTER, /* FILTER */ + EXPR_KIND_WINDOW_PARTITION, /* window definition PARTITION BY */ + EXPR_KIND_WINDOW_ORDER, /* window definition ORDER BY */ + EXPR_KIND_WINDOW_FRAME_RANGE, /* window frame clause with RANGE */ + EXPR_KIND_WINDOW_FRAME_ROWS, /* window frame clause with ROWS */ + EXPR_KIND_WINDOW_FRAME_GROUPS, /* window frame clause with GROUPS */ + EXPR_KIND_SELECT_TARGET, /* SELECT target list item */ + EXPR_KIND_INSERT_TARGET, /* INSERT target list item */ + EXPR_KIND_UPDATE_SOURCE, /* UPDATE assignment source item */ + EXPR_KIND_UPDATE_TARGET, /* UPDATE assignment target item */ + EXPR_KIND_GROUP_BY, /* GROUP BY */ + EXPR_KIND_ORDER_BY, /* ORDER BY */ + EXPR_KIND_DISTINCT_ON, /* DISTINCT ON */ + EXPR_KIND_LIMIT, /* LIMIT */ + EXPR_KIND_OFFSET, /* OFFSET */ + EXPR_KIND_RETURNING, /* RETURNING */ + EXPR_KIND_VALUES, /* VALUES */ + EXPR_KIND_VALUES_SINGLE, /* single-row VALUES (in INSERT only) */ + EXPR_KIND_CHECK_CONSTRAINT, /* CHECK constraint for a table */ + EXPR_KIND_DOMAIN_CHECK, /* CHECK constraint for a domain */ + EXPR_KIND_COLUMN_DEFAULT, /* default value for a table column */ + EXPR_KIND_FUNCTION_DEFAULT, /* default parameter value for function */ + EXPR_KIND_INDEX_EXPRESSION, /* index expression */ + EXPR_KIND_INDEX_PREDICATE, /* index predicate */ + EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */ + EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */ + EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */ + EXPR_KIND_POLICY, /* USING or WITH CHECK expr in policy */ + 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_GENERATED_COLUMN, /* generation expression for a column */ +} ParseExprKind; + /* * Function signatures for parser hooks */ @@ -99,6 +155,8 @@ struct ParseState { List* p_future_ctes; /* common table exprs not yet in namespace */ CommonTableExpr* p_parent_cte; /* this query's containing CTE */ List* p_windowdefs; /* raw representations of window clauses */ + ParseExprKind p_expr_kind; /* what kind of expression we're parsing */ + List* p_rawdefaultlist; /* raw default list */ int p_next_resno; /* next targetlist resno to assign */ List* p_locking_clause; /* raw FOR UPDATE/FOR SHARE info */ Node* p_value_substitute; /* what to replace VALUE with, if any */ diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h index eef0dbae6..adc749bc7 100644 --- a/src/include/parser/parse_utilcmd.h +++ b/src/include/parser/parse_utilcmd.h @@ -53,6 +53,7 @@ typedef struct { Oid bucketOid; /* bucket oid of the resizing table */ List *relnodelist; /* filenode of the resizing table */ List *toastnodelist; /* toast node of the resizing table */ + bool ofType; /* true if statement contains OF typename */ } CreateStmtContext; extern void checkPartitionSynax(CreateStmt *stmt); diff --git a/src/include/utils/be_module.h b/src/include/utils/be_module.h index e45e5bb10..1f522ee7f 100644 --- a/src/include/utils/be_module.h +++ b/src/include/utils/be_module.h @@ -101,6 +101,7 @@ enum ModuleId { MOD_HOTKEY, /* hotkey */ MOD_THREAD_POOL, /* thread_pool */ MOD_OPT_AI, /* ai optimizer */ + MOD_GEN_COL, /* generated column */ /* add your module id above */ MOD_MAX diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index a1ab5d11b..364acbc88 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -51,6 +51,7 @@ extern Oid get_opfamily_proc(Oid opfamily, Oid lefttype, Oid righttype, int16 pr extern char* get_attname(Oid relid, AttrNumber attnum); extern char* get_relid_attribute_name(Oid relid, AttrNumber attnum); extern AttrNumber get_attnum(Oid relid, const char* attname); +extern char GetGenerated(Oid relid, AttrNumber attnum); extern Oid get_atttype(Oid relid, AttrNumber attnum); extern int32 get_atttypmod(Oid relid, AttrNumber attnum); extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum, Oid* typid, int32* typmod, Oid* collid); diff --git a/src/test/regress/expected/alter_table_000.out b/src/test/regress/expected/alter_table_000.out index 82264ed0c..d1ea08af5 100644 --- a/src/test/regress/expected/alter_table_000.out +++ b/src/test/regress/expected/alter_table_000.out @@ -66,6 +66,7 @@ where column_constraint can be: NULL | CHECK ( expression ) | DEFAULT default_expr | + GENERATED ALWAYS AS ( generation_expr ) STORED | UNIQUE index_parameters | PRIMARY KEY index_parameters | REFERENCES reftable [ ( refcolumn ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] diff --git a/src/test/regress/expected/generated_col.out b/src/test/regress/expected/generated_col.out new file mode 100644 index 000000000..26f2a52a5 --- /dev/null +++ b/src/test/regress/expected/generated_col.out @@ -0,0 +1,930 @@ +---------------------------------------------------------- +---------------generated column test +---------------------------------------------------------- +CREATE TABLE gtest0 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (55) STORED); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest0_pkey" for table "gtest0" +CREATE TABLE gtest1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest1_pkey" for table "gtest1" +SELECT table_name, column_name, column_default, is_nullable, is_generated, generation_expression FROM information_schema.columns WHERE table_name LIKE 'gtest_' ORDER BY 1, 2; + table_name | column_name | column_default | is_nullable | is_generated | generation_expression +------------+-------------+----------------+-------------+--------------+----------------------- + gtest0 | a | | NO | NEVER | + gtest0 | b | | YES | ALWAYS | 55 + gtest1 | a | | NO | NEVER | + gtest1 | b | | YES | ALWAYS | (a * 2) +(4 rows) + +\d gtest1 + Table "public.gtest1" + Column | Type | Modifiers +--------+---------+-------------------------------------- + a | integer | not null + b | integer | generated always as ((a * 2)) stored +Indexes: + "gtest1_pkey" PRIMARY KEY, btree (a) TABLESPACE pg_default + +-- duplicate generated +CREATE TABLE gtest_err_1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED GENERATED ALWAYS AS (a * 3) STORED); +ERROR: multiple generation clauses specified for column "b" of table "gtest_err_1" +LINE 1: ...ARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED GENERATED ... + ^ +-- references to other generated columns, including self-references +CREATE TABLE gtest_err_2a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (b * 2) STORED); +ERROR: cannot use generated column "b" in column generation expression +DETAIL: A generated column cannot reference another generated column. +CREATE TABLE gtest_err_2b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (b * 3) STORED); +ERROR: cannot use generated column "b" in column generation expression +DETAIL: A generated column cannot reference another generated column. +-- invalid reference +CREATE TABLE gtest_err_3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) STORED); +ERROR: column "c" does not exist +-- generation expression must be immutable +CREATE TABLE gtest_err_4 (a int PRIMARY KEY, b double precision GENERATED ALWAYS AS (random()) STORED); +ERROR: generation expression is not immutable +-- cannot have default/identity and generated +CREATE TABLE gtest_err_5a (a int PRIMARY KEY, b int DEFAULT 5 GENERATED ALWAYS AS (a * 2) STORED); +ERROR: both default and generation expression specified for column "b" of table "gtest_err_5a" +LINE 1: ... gtest_err_5a (a int PRIMARY KEY, b int DEFAULT 5 GENERATED ... + ^ +CREATE TABLE gtest_err_5b (a int PRIMARY KEY, b int GENERATED ALWAYS AS identity GENERATED ALWAYS AS (a * 2) STORED); +ERROR: syntax error at or near "identity" +LINE 1: ..._5b (a int PRIMARY KEY, b int GENERATED ALWAYS AS identity G... + ^ +-- reference to system column not allowed in generated column +CREATE TABLE gtest_err_6a (a int PRIMARY KEY, b bool GENERATED ALWAYS AS (xmin <> 37) STORED); +ERROR: cannot use system column "xmin" in column generation expression +-- various prohibited constructs +CREATE TABLE gtest_err_7a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (avg(a)) STORED); +ERROR: cannot use aggregate function in generated column expression +CREATE TABLE gtest_err_7b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (row_number() OVER (ORDER BY a)) STORED); +ERROR: cannot use window function in generated column expression +CREATE TABLE gtest_err_7c (a int PRIMARY KEY, b int GENERATED ALWAYS AS ((SELECT a)) STORED); +ERROR: cannot use subquery in generated column expression +CREATE TABLE gtest_err_7d (a int PRIMARY KEY, b int GENERATED ALWAYS AS (generate_series(1, a)) STORED); +ERROR: generated column expression must not return a set +INSERT INTO gtest1 VALUES (1); +INSERT INTO gtest1 VALUES (2, DEFAULT); +INSERT INTO gtest1 VALUES (21, DEFAULT), (22, DEFAULT), (23, DEFAULT), (24, DEFAULT); +DELETE FROM gtest1 WHERE a >20; +INSERT INTO gtest1 VALUES (3, 33); -- error +ERROR: cannot insert into column "b" +DETAIL: Column "b" is a generated column. +SELECT * FROM gtest1 ORDER BY a; + a | b +---+--- + 1 | 2 + 2 | 4 +(2 rows) + +UPDATE gtest1 SET b = DEFAULT WHERE a = 1; +UPDATE gtest1 SET b = 11 WHERE a = 1; -- error +ERROR: column "b" can only be updated to DEFAULT +DETAIL: Column "b" is a generated column. +SELECT * FROM gtest1 ORDER BY a; + a | b +---+--- + 1 | 2 + 2 | 4 +(2 rows) + +SELECT a, b, b * 2 AS b2 FROM gtest1 ORDER BY a; + a | b | b2 +---+---+---- + 1 | 2 | 4 + 2 | 4 | 8 +(2 rows) + +SELECT a, b FROM gtest1 WHERE b = 4 ORDER BY a; + a | b +---+--- + 2 | 4 +(1 row) + +-- test that overflow error happens on write +INSERT INTO gtest1 VALUES (2000000000); +ERROR: integer out of range +SELECT * FROM gtest1; + a | b +---+--- + 2 | 4 + 1 | 2 +(2 rows) + +DELETE FROM gtest1 WHERE a = 2000000000; +-- test with joins +CREATE TABLE gtestx (x int, y int); +INSERT INTO gtestx VALUES (11, 1), (22, 2), (33, 3); +SELECT * FROM gtestx, gtest1 WHERE gtestx.y = gtest1.a; + x | y | a | b +----+---+---+--- + 11 | 1 | 1 | 2 + 22 | 2 | 2 | 4 +(2 rows) + +DROP TABLE gtestx; +-- test UPDATE/DELETE quals +SELECT * FROM gtest1 ORDER BY a; + a | b +---+--- + 1 | 2 + 2 | 4 +(2 rows) + +UPDATE gtest1 SET a = 3 WHERE b = 4; +SELECT * FROM gtest1 ORDER BY a; + a | b +---+--- + 1 | 2 + 3 | 6 +(2 rows) + +DELETE FROM gtest1 WHERE b = 2; +SELECT * FROM gtest1 ORDER BY a; + a | b +---+--- + 3 | 6 +(1 row) + +-- views +CREATE VIEW gtest1v AS SELECT * FROM gtest1; +SELECT * FROM gtest1v; + a | b +---+--- + 3 | 6 +(1 row) + +INSERT INTO gtest1v VALUES (4, 8); -- fails +ERROR: cannot insert into view "gtest1v" +HINT: You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger. +DROP VIEW gtest1v; +-- CTEs +WITH foo AS (SELECT * FROM gtest1) SELECT * FROM foo; + a | b +---+--- + 3 | 6 +(1 row) + +-- test stored update +CREATE TABLE gtest3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 3) STORED); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest3_pkey" for table "gtest3" +INSERT INTO gtest3 (a) VALUES (1), (2), (3); +SELECT * FROM gtest3 ORDER BY a; + a | b +---+--- + 1 | 3 + 2 | 6 + 3 | 9 +(3 rows) + +UPDATE gtest3 SET a = 22 WHERE a = 2; +SELECT * FROM gtest3 ORDER BY a; + a | b +----+---- + 1 | 3 + 3 | 9 + 22 | 66 +(3 rows) + +-- COPY +TRUNCATE gtest1; +INSERT INTO gtest1 (a) VALUES (1), (2); +COPY gtest1 TO stdout; +1 +2 +COPY gtest1 (a, b) TO stdout; +ERROR: column "b" is a generated column +DETAIL: Generated columns cannot be used in COPY. +COPY gtest1 FROM stdin; +COPY gtest1 (a, b) FROM stdin; +ERROR: column "b" is a generated column +DETAIL: Generated columns cannot be used in COPY. +SELECT * FROM gtest1 ORDER BY a; + a | b +---+--- + 1 | 2 + 2 | 4 + 3 | 6 + 4 | 8 +(4 rows) + +TRUNCATE gtest3; +INSERT INTO gtest3 (a) VALUES (1), (2); +COPY gtest3 TO stdout; +1 +2 +COPY gtest3 (a, b) TO stdout; +ERROR: column "b" is a generated column +DETAIL: Generated columns cannot be used in COPY. +COPY gtest3 FROM stdin; +COPY gtest3 (a, b) FROM stdin; +ERROR: column "b" is a generated column +DETAIL: Generated columns cannot be used in COPY. +SELECT * FROM gtest3 ORDER BY a; + a | b +---+---- + 1 | 3 + 2 | 6 + 3 | 9 + 4 | 12 +(4 rows) + +-- null values +CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest2_pkey" for table "gtest2" +INSERT INTO gtest2 VALUES (1); +SELECT * FROM gtest2; + a | b +---+--- + 1 | +(1 row) + +-- composite types +CREATE TYPE double_int as (a int, b int); +CREATE TABLE gtest4 ( + a int, + b double_int GENERATED ALWAYS AS ((a * 2, a * 3)) STORED +); +INSERT INTO gtest4 VALUES (1), (6); +SELECT * FROM gtest4; + a | b +---+--------- + 1 | (2,3) + 6 | (12,18) +(2 rows) + +DROP TABLE gtest4; +DROP TYPE double_int; +-- using tableoid is allowed +CREATE TABLE gtest_tableoid ( + a int PRIMARY KEY, + b bool GENERATED ALWAYS AS (tableoid <> 0) STORED +); +ERROR: cannot use system column "tableoid" in column generation expression +-- drop column behavior +CREATE TABLE gtest10 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) STORED); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest10_pkey" for table "gtest10" +ALTER TABLE gtest10 DROP COLUMN b; +\d gtest10 + Table "public.gtest10" + Column | Type | Modifiers +--------+---------+----------- + a | integer | not null +Indexes: + "gtest10_pkey" PRIMARY KEY, btree (a) TABLESPACE pg_default + +CREATE TABLE gtest10a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest10a_pkey" for table "gtest10a" +ALTER TABLE gtest10a DROP COLUMN b; +INSERT INTO gtest10a (a) VALUES (1); +DROP TABLE gtest10a; +-- privileges +CREATE USER regress_user11 with password 'Gauss_123'; +CREATE TABLE gtest11s (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) STORED); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest11s_pkey" for table "gtest11s" +INSERT INTO gtest11s VALUES (1, 10), (2, 20); +GRANT SELECT (a, c) ON gtest11s TO regress_user11; +CREATE FUNCTION gf1(a int) RETURNS int AS $$ SELECT a * 3 $$ IMMUTABLE LANGUAGE SQL; +REVOKE ALL ON FUNCTION gf1(int) FROM PUBLIC; +CREATE TABLE gtest12s (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) STORED); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest12s_pkey" for table "gtest12s" +INSERT INTO gtest12s VALUES (1, 10), (2, 20); +GRANT SELECT (a, c) ON gtest12s TO regress_user11; +SET ROLE regress_user11 PASSWORD 'Gauss_123'; +SELECT a, b FROM gtest11s; -- not allowed +ERROR: permission denied for relation gtest11s +SELECT a, c FROM gtest11s; -- allowed + a | c +---+---- + 1 | 20 + 2 | 40 +(2 rows) + +SELECT gf1(10); -- not allowed +ERROR: permission denied for function gf1 +CONTEXT: referenced column: gf1 +SELECT a, c FROM gtest12s; -- allowed + a | c +---+---- + 1 | 30 + 2 | 60 +(2 rows) + +RESET ROLE; +DROP TABLE gtest11s, gtest12s; +DROP FUNCTION gf1(int); +DROP USER regress_user11; +-- check constraints +CREATE TABLE gtest20 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED CHECK (b < 50)); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest20_pkey" for table "gtest20" +INSERT INTO gtest20 (a) VALUES (10); -- ok +INSERT INTO gtest20 (a) VALUES (30); -- violates constraint +ERROR: new row for relation "gtest20" violates check constraint "gtest20_b_check" +DETAIL: Failing row contains (30, 60). +CREATE TABLE gtest20a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest20a_pkey" for table "gtest20a" +INSERT INTO gtest20a (a) VALUES (10); +INSERT INTO gtest20a (a) VALUES (30); +ALTER TABLE gtest20a ADD CHECK (b < 50); -- fails on existing row +ERROR: check constraint "gtest20a_b_check" is violated by some row +CREATE TABLE gtest20b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest20b_pkey" for table "gtest20b" +INSERT INTO gtest20b (a) VALUES (10); +INSERT INTO gtest20b (a) VALUES (30); +ALTER TABLE gtest20b ADD CONSTRAINT chk CHECK (b < 50) NOT VALID; +ALTER TABLE gtest20b VALIDATE CONSTRAINT chk; -- fails on existing row +ERROR: check constraint "chk" is violated by some row +DROP TABLE gtest20a,gtest20b; +DROP TABLE gtest20; +-- not-null constraints +CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED NOT NULL); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest21a_pkey" for table "gtest21a" +INSERT INTO gtest21a (a) VALUES (1); -- ok +INSERT INTO gtest21a (a) VALUES (0); -- violates constraint +ERROR: null value in column "b" violates not-null constraint +DETAIL: Failing row contains (0, null). +CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest21b_pkey" for table "gtest21b" +ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL; +INSERT INTO gtest21b (a) VALUES (1); -- ok +INSERT INTO gtest21b (a) VALUES (0); -- violates constraint +ERROR: null value in column "b" violates not-null constraint +DETAIL: Failing row contains (0, null). +ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL; +INSERT INTO gtest21b (a) VALUES (0); -- ok now +DROP TABLE gtest21a,gtest21b; +-- index constraints +CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) STORED UNIQUE); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest22a_pkey" for table "gtest22a" +NOTICE: CREATE TABLE / UNIQUE will create implicit index "gtest22a_b_key" for table "gtest22a" +INSERT INTO gtest22a VALUES (2); +INSERT INTO gtest22a VALUES (3); +INSERT INTO gtest22a VALUES (4); +ERROR: duplicate key value violates unique constraint "gtest22a_b_key" +DETAIL: Key (b)=(2) already exists. +CREATE TABLE gtest22b (a int, b int GENERATED ALWAYS AS (a / 2) STORED, PRIMARY KEY (a, b)); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest22b_pkey" for table "gtest22b" +INSERT INTO gtest22b VALUES (2); +INSERT INTO gtest22b VALUES (2); +ERROR: duplicate key value violates unique constraint "gtest22b_pkey" +DETAIL: Key (a, b)=(2, 1) already exists. +DROP TABLE gtest22b,gtest22a; +-- indexes +CREATE TABLE gtest22c (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +CREATE INDEX gtest22c_b_idx ON gtest22c (b); +CREATE INDEX gtest22c_expr_idx ON gtest22c ((b * 3)); +CREATE INDEX gtest22c_pred_idx ON gtest22c (a) WHERE b > 0; +\d gtest22c + Table "public.gtest22c" + Column | Type | Modifiers +--------+---------+-------------------------------------- + a | integer | + b | integer | generated always as ((a * 2)) stored +Indexes: + "gtest22c_b_idx" btree (b) TABLESPACE pg_default + "gtest22c_expr_idx" btree ((b * 3)) TABLESPACE pg_default + "gtest22c_pred_idx" btree (a) TABLESPACE pg_default WHERE b > 0 + +INSERT INTO gtest22c VALUES (1), (2), (3); +SET enable_seqscan TO off; +SET enable_bitmapscan TO off; +EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 4; + QUERY PLAN +--------------------------------------------- + [Bypass] + Index Scan using gtest22c_b_idx on gtest22c + Index Cond: (b = 4) +(3 rows) + +SELECT * FROM gtest22c WHERE b = 4; + a | b +---+--- + 2 | 4 +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 6; + QUERY PLAN +------------------------------------------------ + [Bypass] + Index Scan using gtest22c_expr_idx on gtest22c + Index Cond: ((b * 3) = 6) +(3 rows) + +SELECT * FROM gtest22c WHERE b * 3 = 6; + a | b +---+--- + 1 | 2 +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE a = 1 AND b > 0; + QUERY PLAN +------------------------------------------------ + [Bypass] + Index Scan using gtest22c_pred_idx on gtest22c + Index Cond: (a = 1) +(3 rows) + +SELECT * FROM gtest22c WHERE a = 1 AND b > 0; + a | b +---+--- + 1 | 2 +(1 row) + +RESET enable_seqscan; +RESET enable_bitmapscan; +DROP TABLE gtest22c; +-- foreign keys +CREATE TABLE gtest23a (x int PRIMARY KEY, y int); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest23a_pkey" for table "gtest23a" +INSERT INTO gtest23a VALUES (1, 11), (2, 22), (3, 33); +CREATE TABLE gtest23x (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED REFERENCES gtest23a (x) ON UPDATE CASCADE); -- error +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest23x_pkey" for table "gtest23x" +ERROR: invalid ON UPDATE action for foreign key constraint containing generated column +CREATE TABLE gtest23x (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED REFERENCES gtest23a (x) ON DELETE SET NULL); -- error +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest23x_pkey" for table "gtest23x" +ERROR: invalid ON DELETE action for foreign key constraint containing generated column +CREATE TABLE gtest23b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED REFERENCES gtest23a (x)); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest23b_pkey" for table "gtest23b" +\d gtest23b + Table "public.gtest23b" + Column | Type | Modifiers +--------+---------+-------------------------------------- + a | integer | not null + b | integer | generated always as ((a * 2)) stored +Indexes: + "gtest23b_pkey" PRIMARY KEY, btree (a) TABLESPACE pg_default +Foreign-key constraints: + "gtest23b_b_fkey" FOREIGN KEY (b) REFERENCES gtest23a(x) + +INSERT INTO gtest23b VALUES (1); -- ok +INSERT INTO gtest23b VALUES (5); -- error +ERROR: insert or update on table "gtest23b" violates foreign key constraint "gtest23b_b_fkey" +DETAIL: Key (b)=(10) is not present in table "gtest23a". +DROP TABLE gtest23b; +DROP TABLE gtest23a; +CREATE TABLE gtest23p (x int, y int GENERATED ALWAYS AS (x * 2) STORED, PRIMARY KEY (y)); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest23p_pkey" for table "gtest23p" +INSERT INTO gtest23p VALUES (1), (2), (3); +CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y)); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest23q_pkey" for table "gtest23q" +INSERT INTO gtest23q VALUES (1, 2); -- ok +INSERT INTO gtest23q VALUES (2, 5); -- error +ERROR: insert or update on table "gtest23q" violates foreign key constraint "gtest23q_b_fkey" +DETAIL: Key (b)=(5) is not present in table "gtest23p". +DROP TABLE gtest23q; +-- domains +CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10); +CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a * 2) STORED); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest24_pkey" for table "gtest24" +INSERT INTO gtest24 (a) VALUES (4); -- ok +INSERT INTO gtest24 (a) VALUES (6); -- error +ERROR: value for domain gtestdomain1 violates check constraint "gtestdomain1_check" +DROP TABLE gtest24; +DROP DOMAIN gtestdomain1; +-- typed tables (currently not supported) +CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint); +CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) STORED); +ERROR: generated columns are not supported on typed tables +DROP TYPE gtest_type CASCADE; +-- partitioned table +CREATE TABLE gtest_parent (f1 date NOT NULL, f2 text, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f1) ( PARTITION p1 VALUES LESS THAN ('2016-07-16')); +INSERT INTO gtest_parent (f1, f2) VALUES ('2016-07-15', 1); +SELECT * FROM gtest_parent; + f1 | f2 | f3 +--------------------------+----+---- + Fri Jul 15 00:00:00 2016 | 1 | 2 +(1 row) + +DROP TABLE gtest_parent; +-- generated columns in partition key (not allowed) +CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f3) ( PARTITION p1 VALUES LESS THAN ('2016-07-16')); +ERROR: cannot use generated column in partition key +DETAIL: Column "f3" is a generated column. +-- ALTER TABLE ... ADD COLUMN +CREATE TABLE gtest25 (a int PRIMARY KEY); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest25_pkey" for table "gtest25" +INSERT INTO gtest25 VALUES (3), (4); +ALTER TABLE gtest25 ADD COLUMN b int GENERATED ALWAYS AS (a * 3) STORED; +SELECT * FROM gtest25 ORDER BY a; + a | b +---+---- + 3 | 9 + 4 | 12 +(2 rows) + +ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (b * 4) STORED; -- error +ERROR: cannot use generated column "b" in column generation expression +DETAIL: A generated column cannot reference another generated column. +ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (z * 4) STORED; -- error +ERROR: column "z" does not exist +DROP TABLE gtest25; +-- ALTER TABLE ... ALTER COLUMN +CREATE TABLE gtest27 ( + a int, + b int GENERATED ALWAYS AS (a * 2) STORED +); +INSERT INTO gtest27 (a) VALUES (3), (4); +ALTER TABLE gtest27 ALTER COLUMN a TYPE text; -- error +ERROR: cannot alter type of a column used by a generated column +DETAIL: Column "a" is used by generated column "b". +ALTER TABLE gtest27 ALTER COLUMN b TYPE numeric; +\d gtest27 + Table "public.gtest27" + Column | Type | Modifiers +--------+---------+-------------------------------------- + a | integer | + b | numeric | generated always as ((a * 2)) stored + +SELECT * FROM gtest27; + a | b +---+--- + 3 | 6 + 4 | 8 +(2 rows) + +ALTER TABLE gtest27 ALTER COLUMN b TYPE boolean USING b <> 0; +ALTER TABLE gtest27 ALTER COLUMN b DROP DEFAULT; -- error +ERROR: column "b" of relation "gtest27" is a generated column +\d gtest27 + Table "public.gtest27" + Column | Type | Modifiers +--------+---------+-------------------------------------- + a | integer | + b | boolean | generated always as ((a * 2)) stored + +DROP TABLE gtest27; +-- triggers +CREATE TABLE gtest26 ( + a int PRIMARY KEY, + b int GENERATED ALWAYS AS (a * 2) STORED +); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "gtest26_pkey" for table "gtest26" +CREATE FUNCTION gtest_trigger_func() RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + IF tg_op IN ('DELETE', 'UPDATE') THEN + RAISE INFO '%: %: old = %', TG_NAME, TG_WHEN, OLD; + END IF; + IF tg_op IN ('INSERT', 'UPDATE') THEN + RAISE INFO '%: %: new = %', TG_NAME, TG_WHEN, NEW; + END IF; + IF tg_op = 'DELETE' THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; +END +$$; +CREATE TRIGGER gtest1 BEFORE DELETE OR UPDATE ON gtest26 + FOR EACH ROW + WHEN (OLD.b < 0) -- ok + EXECUTE PROCEDURE gtest_trigger_func(); +CREATE TRIGGER gtest2a BEFORE INSERT OR UPDATE ON gtest26 + FOR EACH ROW + WHEN (NEW.b < 0) -- error + EXECUTE PROCEDURE gtest_trigger_func(); +ERROR: BEFORE trigger's WHEN condition cannot reference NEW generated columns +LINE 3: WHEN (NEW.b < 0) + ^ +DETAIL: Column "b" is a generated column. +CREATE TRIGGER gtest2b BEFORE INSERT OR UPDATE ON gtest26 + FOR EACH ROW + WHEN (NEW.* IS NOT NULL) -- error + EXECUTE PROCEDURE gtest_trigger_func(); +ERROR: BEFORE trigger's WHEN condition cannot reference NEW generated columns +LINE 3: WHEN (NEW.* IS NOT NULL) + ^ +DETAIL: A whole-row reference is used and the table contains generated columns. +CREATE TRIGGER gtest2 BEFORE INSERT ON gtest26 + FOR EACH ROW + WHEN (NEW.a < 0) + EXECUTE PROCEDURE gtest_trigger_func(); +CREATE TRIGGER gtest3 AFTER DELETE OR UPDATE ON gtest26 + FOR EACH ROW + WHEN (OLD.b < 0) -- ok + EXECUTE PROCEDURE gtest_trigger_func(); +CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26 + FOR EACH ROW + WHEN (NEW.b < 0) -- ok + EXECUTE PROCEDURE gtest_trigger_func(); +INSERT INTO gtest26 (a) VALUES (-2), (0), (3); +INFO: gtest2: BEFORE: new = (-2,) +INFO: gtest4: AFTER: new = (-2,-4) +SELECT * FROM gtest26 ORDER BY a; + a | b +----+---- + -2 | -4 + 0 | 0 + 3 | 6 +(3 rows) + +UPDATE gtest26 SET a = a * -2; +INFO: gtest1: BEFORE: old = (-2,-4) +INFO: gtest1: BEFORE: new = (4,) +INFO: gtest3: AFTER: old = (-2,-4) +INFO: gtest3: AFTER: new = (4,8) +INFO: gtest4: AFTER: old = (3,6) +INFO: gtest4: AFTER: new = (-6,-12) +SELECT * FROM gtest26 ORDER BY a; + a | b +----+----- + -6 | -12 + 0 | 0 + 4 | 8 +(3 rows) + +DELETE FROM gtest26 WHERE a = -6; +INFO: gtest1: BEFORE: old = (-6,-12) +INFO: gtest3: AFTER: old = (-6,-12) +SELECT * FROM gtest26 ORDER BY a; + a | b +---+--- + 0 | 0 + 4 | 8 +(2 rows) + +DROP TRIGGER gtest1 ON gtest26; +DROP TRIGGER gtest2 ON gtest26; +DROP TRIGGER gtest3 ON gtest26; +-- Check that an UPDATE of "a" fires the trigger for UPDATE OF b, per +-- SQL standard. +CREATE FUNCTION gtest_trigger_func3() RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + RAISE NOTICE 'OK'; + RETURN NEW; +END +$$; +CREATE TRIGGER gtest11 BEFORE UPDATE OF b ON gtest26 + FOR EACH ROW + EXECUTE PROCEDURE gtest_trigger_func3(); +UPDATE gtest26 SET a = 1 WHERE a = 0; +NOTICE: OK +DROP TRIGGER gtest11 ON gtest26; +TRUNCATE gtest26; +-- check that modifications of stored generated columns in triggers do +-- not get propagated +CREATE FUNCTION gtest_trigger_func4() RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + NEW.a = 10; + NEW.b = 300; + RETURN NEW; +END; +$$; +CREATE TRIGGER gtest12_01 BEFORE UPDATE ON gtest26 + FOR EACH ROW + EXECUTE PROCEDURE gtest_trigger_func(); +CREATE TRIGGER gtest12_02 BEFORE UPDATE ON gtest26 + FOR EACH ROW + EXECUTE PROCEDURE gtest_trigger_func4(); +CREATE TRIGGER gtest12_03 BEFORE UPDATE ON gtest26 + FOR EACH ROW + EXECUTE PROCEDURE gtest_trigger_func(); +INSERT INTO gtest26 (a) VALUES (1); +UPDATE gtest26 SET a = 11 WHERE a = 1; +INFO: gtest12_01: BEFORE: old = (1,2) +INFO: gtest12_01: BEFORE: new = (11,) +INFO: gtest12_03: BEFORE: old = (1,2) +INFO: gtest12_03: BEFORE: new = (10,) +SELECT * FROM gtest26 ORDER BY a; + a | b +----+---- + 10 | 20 +(1 row) + +DROP TABLE gtest26; +DROP FUNCTION gtest_trigger_func4; +DROP FUNCTION gtest_trigger_func3; +DROP FUNCTION gtest_trigger_func; +-- LIKE INCLUDING GENERATED and dropped column handling +CREATE TABLE gtest28a ( + a int, + b int, + c int, + x int GENERATED ALWAYS AS (b * 2) STORED +); +ALTER TABLE gtest28a DROP COLUMN a; +CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED); +\d gtest28* + Table "public.gtest28a" + Column | Type | Modifiers +--------+---------+-------------------------------------- + b | integer | + c | integer | + x | integer | generated always as ((b * 2)) stored + + Table "public.gtest28b" + Column | Type | Modifiers +--------+---------+-------------------------------------- + b | integer | + c | integer | + x | integer | generated always as ((b * 2)) stored + +DROP TABLE gtest28a; +DROP TABLE gtest28b; +CREATE TABLE test_like_gen_1 (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +\d test_like_gen_1 + Table "public.test_like_gen_1" + Column | Type | Modifiers +--------+---------+-------------------------------------- + a | integer | + b | integer | generated always as ((a * 2)) stored + +INSERT INTO test_like_gen_1 (a) VALUES (1); +SELECT * FROM test_like_gen_1; + a | b +---+--- + 1 | 2 +(1 row) + +CREATE TABLE test_like_gen_2 (LIKE test_like_gen_1); +\d test_like_gen_2 +Table "public.test_like_gen_2" + Column | Type | Modifiers +--------+---------+----------- + a | integer | + b | integer | + +INSERT INTO test_like_gen_2 (a) VALUES (1); +SELECT * FROM test_like_gen_2; + a | b +---+--- + 1 | +(1 row) + +CREATE TABLE test_like_gen_3 (LIKE test_like_gen_1 INCLUDING GENERATED); +\d test_like_gen_3 + Table "public.test_like_gen_3" + Column | Type | Modifiers +--------+---------+-------------------------------------- + a | integer | + b | integer | generated always as ((a * 2)) stored + +INSERT INTO test_like_gen_3 (a) VALUES (1); +SELECT * FROM test_like_gen_3; + a | b +---+--- + 1 | 2 +(1 row) + +DROP TABLE test_like_gen_1, test_like_gen_2, test_like_gen_3; +-- function +CREATE TABLE t1(height_cm int,height_in int GENERATED ALWAYS AS (height_cm * 2) STORED); +CREATE OR REPLACE FUNCTION gen_test(a integer) RETURNS int as $$ +declare +b int; +begin + DELETE t1 where height_cm = a; + INSERT INTO t1 values(a); + SELECT height_in INTO b FROM t1 where height_cm = a; + return b; +end; +$$ language plpgsql; +call gen_test(100); + gen_test +---------- + 200 +(1 row) + +drop function gen_test; +DROP TABLE t1; +-- sequence test +CREATE SEQUENCE seq1 START WITH 33; +-- error +CREATE TABLE t1 (id serial,id1 integer GENERATED ALWAYS AS (nextval('seq1')) STORED); +NOTICE: CREATE TABLE will create implicit sequence "t1_id_seq" for serial column "t1.id" +ERROR: generation expression is not immutable +DROP SEQUENCE seq1; +--merage test +CREATE TABLE products_base +( +product_id INTEGER DEFAULT 0, +product_name VARCHAR(60) DEFAULT 'null', +category VARCHAR(60) DEFAULT 'unknown', +total INTEGER DEFAULT '0' +); +INSERT INTO products_base VALUES (1501, 'vivitar 35mm', 'electrncs', 100); +INSERT INTO products_base VALUES (1502, 'olympus is50', 'electrncs', 100); +INSERT INTO products_base VALUES (1600, 'play gym', 'toys', 100); +INSERT INTO products_base VALUES (1601, 'lamaze', 'toys', 100); +INSERT INTO products_base VALUES (1666, 'harry potter', 'dvd', 100); +CREATE TABLE newproducts_base +( +product_id INTEGER DEFAULT 0, +product_name VARCHAR(60) DEFAULT 'null', +category VARCHAR(60) DEFAULT 'unknown', +total INTEGER DEFAULT '0' +); +INSERT INTO newproducts_base VALUES (1502, 'olympus camera', 'electrncs', 200); +INSERT INTO newproducts_base VALUES (1601, 'lamaze', 'toys', 200); +INSERT INTO newproducts_base VALUES (1666, 'harry potter', 'toys', 200); +INSERT INTO newproducts_base VALUES (1700, 'wait interface', 'books', 200); +CREATE TABLE products_row +( +product_id INTEGER DEFAULT 0, +product_name VARCHAR(60) DEFAULT 'null', +category VARCHAR(60) DEFAULT 'unknown', +total INTEGER DEFAULT '0', +height_in int GENERATED ALWAYS AS (total * 2) STORED +); +INSERT INTO products_row SELECT * FROM products_base; +CREATE TABLE newproducts_row +( +product_id INTEGER DEFAULT 0, +product_name VARCHAR(60) DEFAULT 'null', +category VARCHAR(60) DEFAULT 'unknown', +total INTEGER DEFAULT '0', +height_in int GENERATED ALWAYS AS (total * 3) STORED +); +INSERT INTO newproducts_row SELECT * FROM newproducts_base; +MERGE INTO products_row p +USING newproducts_row np +ON p.product_id = np.product_id +WHEN MATCHED THEN + UPDATE SET product_name = np.product_name, category = np.category, total = np.total +WHEN NOT MATCHED THEN + INSERT VALUES (np.product_id, np.product_name, np.category, np.total); +SELECT * FROM products_row; + product_id | product_name | category | total | height_in +------------+----------------+-----------+-------+----------- + 1501 | vivitar 35mm | electrncs | 100 | 200 + 1600 | play gym | toys | 100 | 200 + 1502 | olympus camera | electrncs | 200 | 400 + 1601 | lamaze | toys | 200 | 400 + 1666 | harry potter | toys | 200 | 400 + 1700 | wait interface | books | 200 | 400 +(6 rows) + +CREATE TABLE hw_create_as_test2(C_INT) as SELECT height_in FROM newproducts_row; +SELECT * FROM hw_create_as_test2; + c_int +------- + 600 + 600 + 600 + 600 +(4 rows) + +DROP TABLE hw_create_as_test2; +DROP TABLE products_row; +DROP TABLE newproducts_row; +DROP TABLE newproducts_base; +DROP TABLE products_base; +--bypass test +INSERT INTO gtest1 values (200); +SELECT * FROM gtest1; + a | b +-----+----- + 1 | 2 + 2 | 4 + 3 | 6 + 4 | 8 + 200 | 400 +(5 rows) + +UPDATE gtest1 set a = 300 where a = 200; +SELECT * FROM gtest1; + a | b +-----+----- + 1 | 2 + 2 | 4 + 3 | 6 + 4 | 8 + 300 | 600 +(5 rows) + +--null test +CREATE TABLE t1 (id int,id1 integer GENERATED ALWAYS AS (id*2) STORED); +INSERT INTO t1 values(100); +SELECT * FROM t1; + id | id1 +-----+----- + 100 | 200 +(1 row) + +UPDATE t1 set id = 200; +SELECT * FROM t1; + id | id1 +-----+----- + 200 | 400 +(1 row) + +UPDATE t1 set id = NULL; +SELECT * FROM t1; + id | id1 +----+----- + | +(1 row) + +DROP TABLE t1; +--cstore not support +CREATE TABLE t2(height_cm int,height_in int GENERATED ALWAYS AS (height_cm * 2) STORED) WITH (ORIENTATION = COLUMN); +ERROR: column/timeseries store unsupport constraint "GENERATED COL" +DROP TABLE gtest0; +DROP TABLE gtest1; +DROP TABLE gtest2; +DROP TABLE gtest3; +DROP TABLE gtest10; diff --git a/src/test/regress/parallel_schedule0 b/src/test/regress/parallel_schedule0 index 58fa417a4..6c3732ff9 100644 --- a/src/test/regress/parallel_schedule0 +++ b/src/test/regress/parallel_schedule0 @@ -761,6 +761,10 @@ test: leaky_function_operator #test: gs_guc test: smp + +#generated column test +test: generated_col + test: sequence_cache_test test: procedure_privilege_test diff --git a/src/test/regress/sql/generated_col.sql b/src/test/regress/sql/generated_col.sql new file mode 100644 index 000000000..e0d8e23f0 --- /dev/null +++ b/src/test/regress/sql/generated_col.sql @@ -0,0 +1,580 @@ +---------------------------------------------------------- +---------------generated column test +---------------------------------------------------------- +CREATE TABLE gtest0 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (55) STORED); +CREATE TABLE gtest1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED); + +SELECT table_name, column_name, column_default, is_nullable, is_generated, generation_expression FROM information_schema.columns WHERE table_name LIKE 'gtest_' ORDER BY 1, 2; + +\d gtest1 + +-- duplicate generated +CREATE TABLE gtest_err_1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED GENERATED ALWAYS AS (a * 3) STORED); + +-- references to other generated columns, including self-references +CREATE TABLE gtest_err_2a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (b * 2) STORED); +CREATE TABLE gtest_err_2b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (b * 3) STORED); + +-- invalid reference +CREATE TABLE gtest_err_3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) STORED); + +-- generation expression must be immutable +CREATE TABLE gtest_err_4 (a int PRIMARY KEY, b double precision GENERATED ALWAYS AS (random()) STORED); + +-- cannot have default/identity and generated +CREATE TABLE gtest_err_5a (a int PRIMARY KEY, b int DEFAULT 5 GENERATED ALWAYS AS (a * 2) STORED); +CREATE TABLE gtest_err_5b (a int PRIMARY KEY, b int GENERATED ALWAYS AS identity GENERATED ALWAYS AS (a * 2) STORED); + +-- reference to system column not allowed in generated column +CREATE TABLE gtest_err_6a (a int PRIMARY KEY, b bool GENERATED ALWAYS AS (xmin <> 37) STORED); + +-- various prohibited constructs +CREATE TABLE gtest_err_7a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (avg(a)) STORED); +CREATE TABLE gtest_err_7b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (row_number() OVER (ORDER BY a)) STORED); +CREATE TABLE gtest_err_7c (a int PRIMARY KEY, b int GENERATED ALWAYS AS ((SELECT a)) STORED); +CREATE TABLE gtest_err_7d (a int PRIMARY KEY, b int GENERATED ALWAYS AS (generate_series(1, a)) STORED); + +INSERT INTO gtest1 VALUES (1); +INSERT INTO gtest1 VALUES (2, DEFAULT); +INSERT INTO gtest1 VALUES (21, DEFAULT), (22, DEFAULT), (23, DEFAULT), (24, DEFAULT); +DELETE FROM gtest1 WHERE a >20; +INSERT INTO gtest1 VALUES (3, 33); -- error + +SELECT * FROM gtest1 ORDER BY a; + +UPDATE gtest1 SET b = DEFAULT WHERE a = 1; +UPDATE gtest1 SET b = 11 WHERE a = 1; -- error + +SELECT * FROM gtest1 ORDER BY a; + +SELECT a, b, b * 2 AS b2 FROM gtest1 ORDER BY a; +SELECT a, b FROM gtest1 WHERE b = 4 ORDER BY a; + +-- test that overflow error happens on write +INSERT INTO gtest1 VALUES (2000000000); +SELECT * FROM gtest1; +DELETE FROM gtest1 WHERE a = 2000000000; + +-- test with joins +CREATE TABLE gtestx (x int, y int); +INSERT INTO gtestx VALUES (11, 1), (22, 2), (33, 3); +SELECT * FROM gtestx, gtest1 WHERE gtestx.y = gtest1.a; +DROP TABLE gtestx; + +-- test UPDATE/DELETE quals +SELECT * FROM gtest1 ORDER BY a; +UPDATE gtest1 SET a = 3 WHERE b = 4; +SELECT * FROM gtest1 ORDER BY a; +DELETE FROM gtest1 WHERE b = 2; +SELECT * FROM gtest1 ORDER BY a; + +-- views +CREATE VIEW gtest1v AS SELECT * FROM gtest1; +SELECT * FROM gtest1v; +INSERT INTO gtest1v VALUES (4, 8); -- fails +DROP VIEW gtest1v; + +-- CTEs +WITH foo AS (SELECT * FROM gtest1) SELECT * FROM foo; + +-- test stored update +CREATE TABLE gtest3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 3) STORED); +INSERT INTO gtest3 (a) VALUES (1), (2), (3); +SELECT * FROM gtest3 ORDER BY a; +UPDATE gtest3 SET a = 22 WHERE a = 2; +SELECT * FROM gtest3 ORDER BY a; + +-- COPY +TRUNCATE gtest1; +INSERT INTO gtest1 (a) VALUES (1), (2); + +COPY gtest1 TO stdout; + +COPY gtest1 (a, b) TO stdout; + +COPY gtest1 FROM stdin; +3 +4 +\. + +COPY gtest1 (a, b) FROM stdin; + +SELECT * FROM gtest1 ORDER BY a; + +TRUNCATE gtest3; +INSERT INTO gtest3 (a) VALUES (1), (2); + +COPY gtest3 TO stdout; + +COPY gtest3 (a, b) TO stdout; + +COPY gtest3 FROM stdin; +3 +4 +\. + +COPY gtest3 (a, b) FROM stdin; + +SELECT * FROM gtest3 ORDER BY a; + +-- null values +CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED); +INSERT INTO gtest2 VALUES (1); +SELECT * FROM gtest2; + +-- composite types +CREATE TYPE double_int as (a int, b int); +CREATE TABLE gtest4 ( + a int, + b double_int GENERATED ALWAYS AS ((a * 2, a * 3)) STORED +); +INSERT INTO gtest4 VALUES (1), (6); +SELECT * FROM gtest4; + +DROP TABLE gtest4; +DROP TYPE double_int; + +-- using tableoid is allowed +CREATE TABLE gtest_tableoid ( + a int PRIMARY KEY, + b bool GENERATED ALWAYS AS (tableoid <> 0) STORED +); + +-- drop column behavior +CREATE TABLE gtest10 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) STORED); +ALTER TABLE gtest10 DROP COLUMN b; + +\d gtest10 + +CREATE TABLE gtest10a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED); +ALTER TABLE gtest10a DROP COLUMN b; +INSERT INTO gtest10a (a) VALUES (1); +DROP TABLE gtest10a; + +-- privileges +CREATE USER regress_user11 with password 'Gauss_123'; + +CREATE TABLE gtest11s (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) STORED); +INSERT INTO gtest11s VALUES (1, 10), (2, 20); +GRANT SELECT (a, c) ON gtest11s TO regress_user11; + +CREATE FUNCTION gf1(a int) RETURNS int AS $$ SELECT a * 3 $$ IMMUTABLE LANGUAGE SQL; +REVOKE ALL ON FUNCTION gf1(int) FROM PUBLIC; + +CREATE TABLE gtest12s (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) STORED); +INSERT INTO gtest12s VALUES (1, 10), (2, 20); +GRANT SELECT (a, c) ON gtest12s TO regress_user11; + +SET ROLE regress_user11 PASSWORD 'Gauss_123'; +SELECT a, b FROM gtest11s; -- not allowed +SELECT a, c FROM gtest11s; -- allowed +SELECT gf1(10); -- not allowed +SELECT a, c FROM gtest12s; -- allowed +RESET ROLE; + +DROP TABLE gtest11s, gtest12s; +DROP FUNCTION gf1(int); +DROP USER regress_user11; + +-- check constraints +CREATE TABLE gtest20 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED CHECK (b < 50)); +INSERT INTO gtest20 (a) VALUES (10); -- ok +INSERT INTO gtest20 (a) VALUES (30); -- violates constraint + +CREATE TABLE gtest20a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED); +INSERT INTO gtest20a (a) VALUES (10); +INSERT INTO gtest20a (a) VALUES (30); +ALTER TABLE gtest20a ADD CHECK (b < 50); -- fails on existing row + +CREATE TABLE gtest20b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED); +INSERT INTO gtest20b (a) VALUES (10); +INSERT INTO gtest20b (a) VALUES (30); +ALTER TABLE gtest20b ADD CONSTRAINT chk CHECK (b < 50) NOT VALID; +ALTER TABLE gtest20b VALIDATE CONSTRAINT chk; -- fails on existing row + +DROP TABLE gtest20a,gtest20b; +DROP TABLE gtest20; + +-- not-null constraints +CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED NOT NULL); +INSERT INTO gtest21a (a) VALUES (1); -- ok +INSERT INTO gtest21a (a) VALUES (0); -- violates constraint + +CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED); +ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL; +INSERT INTO gtest21b (a) VALUES (1); -- ok +INSERT INTO gtest21b (a) VALUES (0); -- violates constraint +ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL; +INSERT INTO gtest21b (a) VALUES (0); -- ok now + +DROP TABLE gtest21a,gtest21b; + +-- index constraints +CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) STORED UNIQUE); +INSERT INTO gtest22a VALUES (2); +INSERT INTO gtest22a VALUES (3); +INSERT INTO gtest22a VALUES (4); +CREATE TABLE gtest22b (a int, b int GENERATED ALWAYS AS (a / 2) STORED, PRIMARY KEY (a, b)); +INSERT INTO gtest22b VALUES (2); +INSERT INTO gtest22b VALUES (2); + +DROP TABLE gtest22b,gtest22a; + +-- indexes +CREATE TABLE gtest22c (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +CREATE INDEX gtest22c_b_idx ON gtest22c (b); +CREATE INDEX gtest22c_expr_idx ON gtest22c ((b * 3)); +CREATE INDEX gtest22c_pred_idx ON gtest22c (a) WHERE b > 0; +\d gtest22c + +INSERT INTO gtest22c VALUES (1), (2), (3); +SET enable_seqscan TO off; +SET enable_bitmapscan TO off; +EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 4; +SELECT * FROM gtest22c WHERE b = 4; +EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 6; +SELECT * FROM gtest22c WHERE b * 3 = 6; +EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE a = 1 AND b > 0; +SELECT * FROM gtest22c WHERE a = 1 AND b > 0; +RESET enable_seqscan; +RESET enable_bitmapscan; + +DROP TABLE gtest22c; + +-- foreign keys +CREATE TABLE gtest23a (x int PRIMARY KEY, y int); +INSERT INTO gtest23a VALUES (1, 11), (2, 22), (3, 33); + +CREATE TABLE gtest23x (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED REFERENCES gtest23a (x) ON UPDATE CASCADE); -- error +CREATE TABLE gtest23x (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED REFERENCES gtest23a (x) ON DELETE SET NULL); -- error + +CREATE TABLE gtest23b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED REFERENCES gtest23a (x)); +\d gtest23b + +INSERT INTO gtest23b VALUES (1); -- ok +INSERT INTO gtest23b VALUES (5); -- error + +DROP TABLE gtest23b; +DROP TABLE gtest23a; + +CREATE TABLE gtest23p (x int, y int GENERATED ALWAYS AS (x * 2) STORED, PRIMARY KEY (y)); +INSERT INTO gtest23p VALUES (1), (2), (3); + +CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y)); +INSERT INTO gtest23q VALUES (1, 2); -- ok +INSERT INTO gtest23q VALUES (2, 5); -- error + +DROP TABLE gtest23q; + +-- domains +CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10); +CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a * 2) STORED); +INSERT INTO gtest24 (a) VALUES (4); -- ok +INSERT INTO gtest24 (a) VALUES (6); -- error + +DROP TABLE gtest24; +DROP DOMAIN gtestdomain1; + +-- typed tables (currently not supported) +CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint); +CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) STORED); +DROP TYPE gtest_type CASCADE; + +-- partitioned table +CREATE TABLE gtest_parent (f1 date NOT NULL, f2 text, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f1) ( PARTITION p1 VALUES LESS THAN ('2016-07-16')); +INSERT INTO gtest_parent (f1, f2) VALUES ('2016-07-15', 1); +SELECT * FROM gtest_parent; +DROP TABLE gtest_parent; + +-- generated columns in partition key (not allowed) +CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f3) ( PARTITION p1 VALUES LESS THAN ('2016-07-16')); + +-- ALTER TABLE ... ADD COLUMN +CREATE TABLE gtest25 (a int PRIMARY KEY); +INSERT INTO gtest25 VALUES (3), (4); +ALTER TABLE gtest25 ADD COLUMN b int GENERATED ALWAYS AS (a * 3) STORED; +SELECT * FROM gtest25 ORDER BY a; +ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (b * 4) STORED; -- error +ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (z * 4) STORED; -- error + +DROP TABLE gtest25; + +-- ALTER TABLE ... ALTER COLUMN +CREATE TABLE gtest27 ( + a int, + b int GENERATED ALWAYS AS (a * 2) STORED +); +INSERT INTO gtest27 (a) VALUES (3), (4); +ALTER TABLE gtest27 ALTER COLUMN a TYPE text; -- error +ALTER TABLE gtest27 ALTER COLUMN b TYPE numeric; +\d gtest27 +SELECT * FROM gtest27; +ALTER TABLE gtest27 ALTER COLUMN b TYPE boolean USING b <> 0; + +ALTER TABLE gtest27 ALTER COLUMN b DROP DEFAULT; -- error +\d gtest27 + +DROP TABLE gtest27; + +-- triggers +CREATE TABLE gtest26 ( + a int PRIMARY KEY, + b int GENERATED ALWAYS AS (a * 2) STORED +); + +CREATE FUNCTION gtest_trigger_func() RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + IF tg_op IN ('DELETE', 'UPDATE') THEN + RAISE INFO '%: %: old = %', TG_NAME, TG_WHEN, OLD; + END IF; + IF tg_op IN ('INSERT', 'UPDATE') THEN + RAISE INFO '%: %: new = %', TG_NAME, TG_WHEN, NEW; + END IF; + IF tg_op = 'DELETE' THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; +END +$$; + +CREATE TRIGGER gtest1 BEFORE DELETE OR UPDATE ON gtest26 + FOR EACH ROW + WHEN (OLD.b < 0) -- ok + EXECUTE PROCEDURE gtest_trigger_func(); + +CREATE TRIGGER gtest2a BEFORE INSERT OR UPDATE ON gtest26 + FOR EACH ROW + WHEN (NEW.b < 0) -- error + EXECUTE PROCEDURE gtest_trigger_func(); + +CREATE TRIGGER gtest2b BEFORE INSERT OR UPDATE ON gtest26 + FOR EACH ROW + WHEN (NEW.* IS NOT NULL) -- error + EXECUTE PROCEDURE gtest_trigger_func(); + +CREATE TRIGGER gtest2 BEFORE INSERT ON gtest26 + FOR EACH ROW + WHEN (NEW.a < 0) + EXECUTE PROCEDURE gtest_trigger_func(); + +CREATE TRIGGER gtest3 AFTER DELETE OR UPDATE ON gtest26 + FOR EACH ROW + WHEN (OLD.b < 0) -- ok + EXECUTE PROCEDURE gtest_trigger_func(); + +CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26 + FOR EACH ROW + WHEN (NEW.b < 0) -- ok + EXECUTE PROCEDURE gtest_trigger_func(); + +INSERT INTO gtest26 (a) VALUES (-2), (0), (3); +SELECT * FROM gtest26 ORDER BY a; +UPDATE gtest26 SET a = a * -2; +SELECT * FROM gtest26 ORDER BY a; +DELETE FROM gtest26 WHERE a = -6; +SELECT * FROM gtest26 ORDER BY a; + +DROP TRIGGER gtest1 ON gtest26; +DROP TRIGGER gtest2 ON gtest26; +DROP TRIGGER gtest3 ON gtest26; + +-- Check that an UPDATE of "a" fires the trigger for UPDATE OF b, per +-- SQL standard. +CREATE FUNCTION gtest_trigger_func3() RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + RAISE NOTICE 'OK'; + RETURN NEW; +END +$$; + +CREATE TRIGGER gtest11 BEFORE UPDATE OF b ON gtest26 + FOR EACH ROW + EXECUTE PROCEDURE gtest_trigger_func3(); + +UPDATE gtest26 SET a = 1 WHERE a = 0; + +DROP TRIGGER gtest11 ON gtest26; +TRUNCATE gtest26; + +-- check that modifications of stored generated columns in triggers do +-- not get propagated +CREATE FUNCTION gtest_trigger_func4() RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + NEW.a = 10; + NEW.b = 300; + RETURN NEW; +END; +$$; + +CREATE TRIGGER gtest12_01 BEFORE UPDATE ON gtest26 + FOR EACH ROW + EXECUTE PROCEDURE gtest_trigger_func(); + +CREATE TRIGGER gtest12_02 BEFORE UPDATE ON gtest26 + FOR EACH ROW + EXECUTE PROCEDURE gtest_trigger_func4(); + +CREATE TRIGGER gtest12_03 BEFORE UPDATE ON gtest26 + FOR EACH ROW + EXECUTE PROCEDURE gtest_trigger_func(); + +INSERT INTO gtest26 (a) VALUES (1); +UPDATE gtest26 SET a = 11 WHERE a = 1; +SELECT * FROM gtest26 ORDER BY a; + +DROP TABLE gtest26; +DROP FUNCTION gtest_trigger_func4; +DROP FUNCTION gtest_trigger_func3; +DROP FUNCTION gtest_trigger_func; + + +-- LIKE INCLUDING GENERATED and dropped column handling +CREATE TABLE gtest28a ( + a int, + b int, + c int, + x int GENERATED ALWAYS AS (b * 2) STORED +); + +ALTER TABLE gtest28a DROP COLUMN a; + +CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED); + +\d gtest28* + +DROP TABLE gtest28a; +DROP TABLE gtest28b; + +CREATE TABLE test_like_gen_1 (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +\d test_like_gen_1 +INSERT INTO test_like_gen_1 (a) VALUES (1); +SELECT * FROM test_like_gen_1; +CREATE TABLE test_like_gen_2 (LIKE test_like_gen_1); +\d test_like_gen_2 +INSERT INTO test_like_gen_2 (a) VALUES (1); +SELECT * FROM test_like_gen_2; +CREATE TABLE test_like_gen_3 (LIKE test_like_gen_1 INCLUDING GENERATED); +\d test_like_gen_3 +INSERT INTO test_like_gen_3 (a) VALUES (1); +SELECT * FROM test_like_gen_3; +DROP TABLE test_like_gen_1, test_like_gen_2, test_like_gen_3; + +-- function +CREATE TABLE t1(height_cm int,height_in int GENERATED ALWAYS AS (height_cm * 2) STORED); +CREATE OR REPLACE FUNCTION gen_test(a integer) RETURNS int as $$ +declare +b int; +begin + DELETE t1 where height_cm = a; + INSERT INTO t1 values(a); + SELECT height_in INTO b FROM t1 where height_cm = a; + return b; +end; +$$ language plpgsql; +call gen_test(100); +drop function gen_test; +DROP TABLE t1; + +-- sequence test +CREATE SEQUENCE seq1 START WITH 33; +-- error +CREATE TABLE t1 (id serial,id1 integer GENERATED ALWAYS AS (nextval('seq1')) STORED); +DROP SEQUENCE seq1; + +--merage test +CREATE TABLE products_base +( +product_id INTEGER DEFAULT 0, +product_name VARCHAR(60) DEFAULT 'null', +category VARCHAR(60) DEFAULT 'unknown', +total INTEGER DEFAULT '0' +); + +INSERT INTO products_base VALUES (1501, 'vivitar 35mm', 'electrncs', 100); +INSERT INTO products_base VALUES (1502, 'olympus is50', 'electrncs', 100); +INSERT INTO products_base VALUES (1600, 'play gym', 'toys', 100); +INSERT INTO products_base VALUES (1601, 'lamaze', 'toys', 100); +INSERT INTO products_base VALUES (1666, 'harry potter', 'dvd', 100); + +CREATE TABLE newproducts_base +( +product_id INTEGER DEFAULT 0, +product_name VARCHAR(60) DEFAULT 'null', +category VARCHAR(60) DEFAULT 'unknown', +total INTEGER DEFAULT '0' +); + +INSERT INTO newproducts_base VALUES (1502, 'olympus camera', 'electrncs', 200); +INSERT INTO newproducts_base VALUES (1601, 'lamaze', 'toys', 200); +INSERT INTO newproducts_base VALUES (1666, 'harry potter', 'toys', 200); +INSERT INTO newproducts_base VALUES (1700, 'wait interface', 'books', 200); + +CREATE TABLE products_row +( +product_id INTEGER DEFAULT 0, +product_name VARCHAR(60) DEFAULT 'null', +category VARCHAR(60) DEFAULT 'unknown', +total INTEGER DEFAULT '0', +height_in int GENERATED ALWAYS AS (total * 2) STORED +); +INSERT INTO products_row SELECT * FROM products_base; + +CREATE TABLE newproducts_row +( +product_id INTEGER DEFAULT 0, +product_name VARCHAR(60) DEFAULT 'null', +category VARCHAR(60) DEFAULT 'unknown', +total INTEGER DEFAULT '0', +height_in int GENERATED ALWAYS AS (total * 3) STORED +); +INSERT INTO newproducts_row SELECT * FROM newproducts_base; + +MERGE INTO products_row p +USING newproducts_row np +ON p.product_id = np.product_id +WHEN MATCHED THEN + UPDATE SET product_name = np.product_name, category = np.category, total = np.total +WHEN NOT MATCHED THEN + INSERT VALUES (np.product_id, np.product_name, np.category, np.total); +SELECT * FROM products_row; + +CREATE TABLE hw_create_as_test2(C_INT) as SELECT height_in FROM newproducts_row; +SELECT * FROM hw_create_as_test2; +DROP TABLE hw_create_as_test2; +DROP TABLE products_row; +DROP TABLE newproducts_row; +DROP TABLE newproducts_base; +DROP TABLE products_base; + +--bypass test +INSERT INTO gtest1 values (200); +SELECT * FROM gtest1; +UPDATE gtest1 set a = 300 where a = 200; +SELECT * FROM gtest1; + +--null test +CREATE TABLE t1 (id int,id1 integer GENERATED ALWAYS AS (id*2) STORED); +INSERT INTO t1 values(100); +SELECT * FROM t1; +UPDATE t1 set id = 200; +SELECT * FROM t1; +UPDATE t1 set id = NULL; +SELECT * FROM t1; +DROP TABLE t1; + +--cstore not support +CREATE TABLE t2(height_cm int,height_in int GENERATED ALWAYS AS (height_cm * 2) STORED) WITH (ORIENTATION = COLUMN); + +DROP TABLE gtest0; +DROP TABLE gtest1; +DROP TABLE gtest2; +DROP TABLE gtest3; +DROP TABLE gtest10; +