From ef1448f7852000d5b701f9e3c7fe88737670740a Mon Sep 17 00:00:00 2001 From: Amul Sul Date: Mon, 31 Jul 2023 15:43:51 +0530 Subject: [PATCH v1 2/2] Allow to change generated column expression --- doc/src/sgml/ref/alter_table.sgml | 14 +- src/backend/commands/tablecmds.c | 88 +++++++++---- src/backend/parser/gram.y | 10 ++ src/bin/psql/tab-complete.c | 2 +- src/test/regress/expected/generated.out | 167 ++++++++++++++++++++---- src/test/regress/sql/generated.sql | 36 ++++- 6 files changed, 262 insertions(+), 55 deletions(-) diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index d4d93eeb7c6..1b68dea8d9b 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -46,6 +46,7 @@ ALTER TABLE [ IF EXISTS ] name ALTER [ COLUMN ] column_name SET DEFAULT expression ALTER [ COLUMN ] column_name DROP DEFAULT ALTER [ COLUMN ] column_name { SET | DROP } NOT NULL + ALTER [ COLUMN ] column_name SET EXPRESSION expression ALTER [ COLUMN ] column_name DROP EXPRESSION [ IF EXISTS ] ALTER [ COLUMN ] column_name ADD GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( sequence_options ) ] ALTER [ COLUMN ] column_name { SET GENERATED { ALWAYS | BY DEFAULT } | SET sequence_option | RESTART [ [ WITH ] restart ] } [...] @@ -255,13 +256,18 @@ WITH ( MODULUS numeric_literal, REM - + DROP EXPRESSION [ IF EXISTS ] - This form turns a stored generated column into a normal base column. - Existing data in the columns is retained, but future changes will no - longer apply the generation expression. + The SET form replaces stored generated value for a + column. Existing data in the columns is rewritten and all the future + changes will apply the new generation expression. + + + The DROP form turns a stored generated column into a + normal base column. Existing data in the columns is retained, but future + changes will no longer apply the generation expression. diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 3b499fc0d8e..df26afe16cb 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -456,7 +456,8 @@ static ObjectAddress ATExecSetIdentity(Relation rel, const char *colName, static ObjectAddress ATExecDropIdentity(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode); static void ATPrepColumnExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recursing, LOCKMODE lockmode); -static ObjectAddress ATExecColumnExpression(Relation rel, const char *colName, +static ObjectAddress ATExecColumnExpression(AlteredTableInfo *tab, Relation rel, + const char *colName, Node *newDefault, bool missing_ok, LOCKMODE lockmode); static ObjectAddress ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newValue, LOCKMODE lockmode); @@ -5056,7 +5057,8 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, ATExecCheckNotNull(tab, rel, cmd->name, lockmode); break; case AT_ColumnExpression: - address = ATExecColumnExpression(rel, cmd->name, cmd->missing_ok, lockmode); + address = ATExecColumnExpression(tab, rel, cmd->name, cmd->def, + cmd->missing_ok, lockmode); break; case AT_SetStatistics: /* ALTER COLUMN SET STATISTICS */ address = ATExecSetStatistics(rel, cmd->name, cmd->num, cmd->def, lockmode); @@ -8015,16 +8017,21 @@ static void ATPrepColumnExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recursing, LOCKMODE lockmode) { /* - * Reject ONLY if there are child tables. We could implement this, but it - * is a bit complicated. GENERATED clauses must be attached to the column - * definition and cannot be added later like DEFAULT, so if a child table - * has a generation expression that the parent does not have, the child - * column will necessarily be an attislocal column. So to implement ONLY - * here, we'd need extra code to update attislocal of the direct child - * tables, somewhat similar to how DROP COLUMN does it, so that the - * resulting state can be properly dumped and restored. + * Only SET EXPRESSION would be having new expression for the replacement. */ - if (!recurse && + bool isdrop = (cmd->def == NULL); + + /* + * Reject ALTER TABLE ONLY ... DROP EXPRESSION if there are child tables. + * We could implement this, but it is a bit complicated. GENERATED clauses + * must be attached to the column definition and cannot be added later like + * DEFAULT, so if a child table has a generation expression that the parent + * does not have, the child column will necessarily be an attislocal column. + * So to implement ONLY here, we'd need extra code to update attislocal of + * the direct child tables, somewhat similar to how DROP COLUMN does it, so + * that the resulting state can be properly dumped and restored. + */ + if (!recurse && isdrop && find_inheritance_children(RelationGetRelid(rel), lockmode)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -8047,7 +8054,7 @@ ATPrepColumnExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recu attTup = (Form_pg_attribute) GETSTRUCT(tuple); - if (attTup->attinhcount > 0) + if (attTup->attinhcount > 0 && isdrop) ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("cannot drop generation expression from inherited column"))); @@ -8058,7 +8065,8 @@ ATPrepColumnExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recu * Return the address of the affected column. */ static ObjectAddress -ATExecColumnExpression(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode) +ATExecColumnExpression(AlteredTableInfo *tab, Relation rel, const char *colName, + Node *newDefault, bool missing_ok, LOCKMODE lockmode) { HeapTuple tuple; Form_pg_attribute attTup; @@ -8102,16 +8110,21 @@ ATExecColumnExpression(Relation rel, const char *colName, bool missing_ok, LOCKM } } - /* - * Mark the column as no longer generated. (The atthasdef flag needs to - * get cleared too, but RemoveAttrDefault will handle that.) - */ - attTup->attgenerated = '\0'; - CatalogTupleUpdate(attrelation, &tuple->t_self, tuple); + /* DROP EXPRESSION */ + if (newDefault == NULL) + { + /* + * Mark the column as no longer generated. (The atthasdef flag needs to + * get cleared too, but RemoveAttrDefault will handle that.) + */ + attTup->attgenerated = '\0'; + CatalogTupleUpdate(attrelation, &tuple->t_self, tuple); + + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), + attnum); + } - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), - attnum); heap_freetuple(tuple); table_close(attrelation, RowExclusiveLock); @@ -8138,6 +8151,37 @@ ATExecColumnExpression(Relation rel, const char *colName, bool missing_ok, LOCKM RemoveAttrDefault(RelationGetRelid(rel), attnum, DROP_RESTRICT, false, false); + /* SET EXPRESSION */ + if (newDefault) + { + Expr *defval; + NewColumnValue *newval; + RawColumnDefault *rawEnt; + + /* Prepare to store the EXPRESSION, in the catalogs */ + rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault)); + rawEnt->attnum = attnum; + rawEnt->raw_default = newDefault; + rawEnt->missingMode = false; + rawEnt->generated = ATTRIBUTE_GENERATED_STORED; + + /* Store the EXPRESSION */ + AddRelationNewConstraints(rel, list_make1(rawEnt), NIL, + false, true, false, NULL); + CommandCounterIncrement(); + + /* Prepare for table rewrite */ + defval = (Expr *) build_column_default(rel, attnum); + + newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue)); + newval->attnum = attnum; + newval->expr = expression_planner(defval); + newval->is_generated = true; + + tab->newvals = lappend(tab->newvals, newval); + tab->rewrite |= AT_REWRITE_DEFAULT_VAL; + } + ObjectAddressSubSet(address, RelationRelationId, RelationGetRelid(rel), attnum); return address; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index b721fc88dee..c5bcd82399f 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2404,6 +2404,16 @@ alter_table_cmd: n->name = $3; $$ = (Node *) n; } + /* ALTER TABLE ALTER [COLUMN] SET EXPRESSION */ + | ALTER opt_column ColId SET EXPRESSION a_expr + { + AlterTableCmd *n = makeNode(AlterTableCmd); + + n->subtype = AT_ColumnExpression; + n->name = $3; + n->def = $6; + $$ = (Node *) n; + } /* ALTER TABLE ALTER [COLUMN] DROP EXPRESSION */ | ALTER opt_column ColId DROP EXPRESSION { diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 779fdc90cbc..eac074ffc1f 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -2483,7 +2483,7 @@ psql_completion(const char *text, int start, int end) /* ALTER TABLE ALTER [COLUMN] SET */ else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET") || Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET")) - COMPLETE_WITH("(", "COMPRESSION", "DEFAULT", "GENERATED", "NOT NULL", "STATISTICS", "STORAGE", + COMPLETE_WITH("(", "COMPRESSION", "EXPRESSION", "DEFAULT", "GENERATED", "NOT NULL", "STATISTICS", "STORAGE", /* a subset of ALTER SEQUENCE options */ "INCREMENT", "MINVALUE", "MAXVALUE", "START", "NO", "CACHE", "CYCLE"); /* ALTER TABLE ALTER [COLUMN] SET ( */ diff --git a/src/test/regress/expected/generated.out b/src/test/regress/expected/generated.out index f5d802b9d14..5dd4edc084f 100644 --- a/src/test/regress/expected/generated.out +++ b/src/test/regress/expected/generated.out @@ -780,30 +780,119 @@ Partition of: gtest_parent FOR VALUES FROM ('08-01-2016') TO ('09-01-2016') Partition of: gtest_parent FOR VALUES FROM ('09-01-2016') TO ('10-01-2016') INSERT INTO gtest_parent (f1, f2) VALUES ('2016-07-15', 1); -SELECT * FROM gtest_parent; - f1 | f2 | f3 -------------+----+---- - 07-15-2016 | 1 | 2 -(1 row) +INSERT INTO gtest_parent (f1, f2) VALUES ('2016-07-15', 2); +INSERT INTO gtest_parent (f1, f2) VALUES ('2016-08-15', 3); +SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1; + tableoid | f1 | f2 | f3 +--------------+------------+----+---- + gtest_child | 07-15-2016 | 1 | 2 + gtest_child | 07-15-2016 | 2 | 4 + gtest_child2 | 08-15-2016 | 3 | 66 +(3 rows) -SELECT * FROM gtest_child; - f1 | f2 | f3 -------------+----+---- - 07-15-2016 | 1 | 2 -(1 row) +UPDATE gtest_parent SET f1 = f1 + 60 WHERE f2 = 1; +SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1; + tableoid | f1 | f2 | f3 +--------------+------------+----+---- + gtest_child | 07-15-2016 | 2 | 4 + gtest_child2 | 08-15-2016 | 3 | 66 + gtest_child3 | 09-13-2016 | 1 | 33 +(3 rows) -UPDATE gtest_parent SET f1 = f1 + 60; -SELECT * FROM gtest_parent; - f1 | f2 | f3 -------------+----+---- - 09-13-2016 | 1 | 33 -(1 row) +-- alter only parent's and one child's generated expression +ALTER TABLE ONLY gtest_parent ALTER COLUMN f3 SET EXPRESSION (f2 * 4); +ALTER TABLE gtest_child ALTER COLUMN f3 SET EXPRESSION (f2 * 10); +\d gtest_parent + Partitioned table "public.gtest_parent" + Column | Type | Collation | Nullable | Default +--------+--------+-----------+----------+------------------------------------- + f1 | date | | not null | + f2 | bigint | | | + f3 | bigint | | | generated always as (f2 * 4) stored +Partition key: RANGE (f1) +Number of partitions: 3 (Use \d+ to list them.) -SELECT * FROM gtest_child3; - f1 | f2 | f3 -------------+----+---- - 09-13-2016 | 1 | 33 -(1 row) +\d gtest_child + Table "public.gtest_child" + Column | Type | Collation | Nullable | Default +--------+--------+-----------+----------+-------------------------------------- + f1 | date | | not null | + f2 | bigint | | | + f3 | bigint | | | generated always as (f2 * 10) stored +Partition of: gtest_parent FOR VALUES FROM ('07-01-2016') TO ('08-01-2016') + +\d gtest_child2 + Table "public.gtest_child2" + Column | Type | Collation | Nullable | Default +--------+--------+-----------+----------+-------------------------------------- + f1 | date | | not null | + f2 | bigint | | | + f3 | bigint | | | generated always as (f2 * 22) stored +Partition of: gtest_parent FOR VALUES FROM ('08-01-2016') TO ('09-01-2016') + +\d gtest_child3 + Table "public.gtest_child3" + Column | Type | Collation | Nullable | Default +--------+--------+-----------+----------+-------------------------------------- + f1 | date | | not null | + f2 | bigint | | | + f3 | bigint | | | generated always as (f2 * 33) stored +Partition of: gtest_parent FOR VALUES FROM ('09-01-2016') TO ('10-01-2016') + +SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1; + tableoid | f1 | f2 | f3 +--------------+------------+----+---- + gtest_child | 07-15-2016 | 2 | 20 + gtest_child2 | 08-15-2016 | 3 | 66 + gtest_child3 | 09-13-2016 | 1 | 33 +(3 rows) + +-- alter generated expression of a parent and all it's child altogether +ALTER TABLE gtest_parent ALTER COLUMN f3 SET EXPRESSION (f2 * 2); +\d gtest_parent + Partitioned table "public.gtest_parent" + Column | Type | Collation | Nullable | Default +--------+--------+-----------+----------+------------------------------------- + f1 | date | | not null | + f2 | bigint | | | + f3 | bigint | | | generated always as (f2 * 2) stored +Partition key: RANGE (f1) +Number of partitions: 3 (Use \d+ to list them.) + +\d gtest_child + Table "public.gtest_child" + Column | Type | Collation | Nullable | Default +--------+--------+-----------+----------+------------------------------------- + f1 | date | | not null | + f2 | bigint | | | + f3 | bigint | | | generated always as (f2 * 2) stored +Partition of: gtest_parent FOR VALUES FROM ('07-01-2016') TO ('08-01-2016') + +\d gtest_child2 + Table "public.gtest_child2" + Column | Type | Collation | Nullable | Default +--------+--------+-----------+----------+------------------------------------- + f1 | date | | not null | + f2 | bigint | | | + f3 | bigint | | | generated always as (f2 * 2) stored +Partition of: gtest_parent FOR VALUES FROM ('08-01-2016') TO ('09-01-2016') + +\d gtest_child3 + Table "public.gtest_child3" + Column | Type | Collation | Nullable | Default +--------+--------+-----------+----------+------------------------------------- + f1 | date | | not null | + f2 | bigint | | | + f3 | bigint | | | generated always as (f2 * 2) stored +Partition of: gtest_parent FOR VALUES FROM ('09-01-2016') TO ('10-01-2016') + +SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1; + tableoid | f1 | f2 | f3 +--------------+------------+----+---- + gtest_child | 07-15-2016 | 2 | 4 + gtest_child2 | 08-15-2016 | 3 | 6 + gtest_child3 | 09-13-2016 | 1 | 2 +(3 rows) -- we leave these tables around for purposes of testing dump/reload/upgrade -- generated columns in partition key (not allowed) @@ -930,18 +1019,50 @@ CREATE TABLE gtest29 ( b int GENERATED ALWAYS AS (a * 2) STORED ); INSERT INTO gtest29 (a) VALUES (3), (4); +SELECT * FROM gtest29; + a | b +---+--- + 3 | 6 + 4 | 8 +(2 rows) + +\d gtest29 + Table "public.gtest29" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+------------------------------------ + a | integer | | | + b | integer | | | generated always as (a * 2) stored + +ALTER TABLE gtest29 ALTER COLUMN a SET EXPRESSION (a * 3); -- error +ERROR: column "a" of relation "gtest29" is not a stored generated column ALTER TABLE gtest29 ALTER COLUMN a DROP EXPRESSION; -- error ERROR: column "a" of relation "gtest29" is not a stored generated column ALTER TABLE gtest29 ALTER COLUMN a DROP EXPRESSION IF EXISTS; -- notice NOTICE: column "a" of relation "gtest29" is not a stored generated column, skipping +-- Change the expression +ALTER TABLE gtest29 ALTER COLUMN b SET EXPRESSION (a * 3); +SELECT * FROM gtest29; + a | b +---+---- + 3 | 9 + 4 | 12 +(2 rows) + +\d gtest29 + Table "public.gtest29" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+------------------------------------ + a | integer | | | + b | integer | | | generated always as (a * 3) stored + ALTER TABLE gtest29 ALTER COLUMN b DROP EXPRESSION; INSERT INTO gtest29 (a) VALUES (5); INSERT INTO gtest29 (a, b) VALUES (6, 66); SELECT * FROM gtest29; a | b ---+---- - 3 | 6 - 4 | 8 + 3 | 9 + 4 | 12 5 | 6 | 66 (4 rows) diff --git a/src/test/regress/sql/generated.sql b/src/test/regress/sql/generated.sql index 8ddecf0cc38..e510f77cc33 100644 --- a/src/test/regress/sql/generated.sql +++ b/src/test/regress/sql/generated.sql @@ -411,11 +411,28 @@ ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09 \d gtest_child2 \d gtest_child3 INSERT INTO gtest_parent (f1, f2) VALUES ('2016-07-15', 1); -SELECT * FROM gtest_parent; -SELECT * FROM gtest_child; -UPDATE gtest_parent SET f1 = f1 + 60; -SELECT * FROM gtest_parent; -SELECT * FROM gtest_child3; +INSERT INTO gtest_parent (f1, f2) VALUES ('2016-07-15', 2); +INSERT INTO gtest_parent (f1, f2) VALUES ('2016-08-15', 3); +SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1; +UPDATE gtest_parent SET f1 = f1 + 60 WHERE f2 = 1; +SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1; + +-- alter only parent's and one child's generated expression +ALTER TABLE ONLY gtest_parent ALTER COLUMN f3 SET EXPRESSION (f2 * 4); +ALTER TABLE gtest_child ALTER COLUMN f3 SET EXPRESSION (f2 * 10); +\d gtest_parent +\d gtest_child +\d gtest_child2 +\d gtest_child3 +SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1; + +-- alter generated expression of a parent and all it's child altogether +ALTER TABLE gtest_parent ALTER COLUMN f3 SET EXPRESSION (f2 * 2); +\d gtest_parent +\d gtest_child +\d gtest_child2 +\d gtest_child3 +SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1; -- we leave these tables around for purposes of testing dump/reload/upgrade -- generated columns in partition key (not allowed) @@ -470,8 +487,17 @@ CREATE TABLE gtest29 ( b int GENERATED ALWAYS AS (a * 2) STORED ); INSERT INTO gtest29 (a) VALUES (3), (4); +SELECT * FROM gtest29; +\d gtest29 +ALTER TABLE gtest29 ALTER COLUMN a SET EXPRESSION (a * 3); -- error ALTER TABLE gtest29 ALTER COLUMN a DROP EXPRESSION; -- error ALTER TABLE gtest29 ALTER COLUMN a DROP EXPRESSION IF EXISTS; -- notice + +-- Change the expression +ALTER TABLE gtest29 ALTER COLUMN b SET EXPRESSION (a * 3); +SELECT * FROM gtest29; +\d gtest29 + ALTER TABLE gtest29 ALTER COLUMN b DROP EXPRESSION; INSERT INTO gtest29 (a) VALUES (5); INSERT INTO gtest29 (a, b) VALUES (6, 66); -- 2.18.0