support ALTER TABLE DROP EXPRESSION for virtual generated column
hi.
the attached patch is to implement $subject.
the generation expression will be dropped.
the column value previous was NULL will be materialized
based on generation expression.
It seems fairly straightforward:
drop the generation expression in ATExecDropExpression,
and instruct phase 3 to compute the generation expression
and do the table rewrite.
the doc changes:
<para>
- 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.
- </para>
-
- <para>
- This form is currently only supported for stored generated columns (not
- virtual ones).
+ This form turns a generated column into a normal base column.
+ For stored generated column, existing data in the columns is retained;
+ For virtual generated column, it will compute the generation
expression and
+ store the value in the columns. For inheritance hierarchy or
partition hierarchy,
+ the virtual generation expression is computed based on the
child's own generation expression.
+ The future changes will no longer apply the generation expression.
</para>
Attachments:
v1-0001-support-ALTER-TABLE-DROP-EXPRESSION-for-virtual-g.patchtext/x-patch; charset=US-ASCII; name=v1-0001-support-ALTER-TABLE-DROP-EXPRESSION-for-virtual-g.patchDownload
From b3b23e5d7fee6143521560790bf4ad14e21e8a49 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Thu, 27 Mar 2025 09:49:29 +0800
Subject: [PATCH v1 1/1] support ALTER TABLE DROP EXPRESSION for virtual
generated column
It seems fairly straightforward:
drop the generation expression in ATExecDropExpression, and instruct phase 3 to
compute the generation expression and do the table rewrite.
discussion: https://postgr.es/m/
commitfest entry:
---
doc/src/sgml/ref/alter_table.sgml | 14 ++--
src/backend/commands/tablecmds.c | 36 +++++----
src/test/regress/expected/fast_default.out | 3 +
.../regress/expected/generated_virtual.out | 81 ++++++++++++++-----
src/test/regress/sql/fast_default.sql | 2 +
src/test/regress/sql/generated_virtual.sql | 26 +++++-
6 files changed, 114 insertions(+), 48 deletions(-)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 4f15b89a98f..cd68697e215 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -275,14 +275,12 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>DROP EXPRESSION [ IF EXISTS ]</literal></term>
<listitem>
<para>
- 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.
- </para>
-
- <para>
- This form is currently only supported for stored generated columns (not
- virtual ones).
+ This form turns a generated column into a normal base column.
+ For stored generated column, existing data in the columns is retained;
+ For virtual generated column, it will compute the generation expression and
+ store the value in the columns. For inheritance hierarchy or partition hierarchy,
+ the virtual generation expression is computed based on the child's own generation expression.
+ The future changes will no longer apply the generation expression.
</para>
<para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1202544ebd0..f1f5abf50de 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -494,7 +494,8 @@ static ObjectAddress ATExecDropIdentity(Relation rel, const char *colName, bool
static ObjectAddress ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
Node *newExpr, LOCKMODE lockmode);
static void ATPrepDropExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recursing, LOCKMODE lockmode);
-static ObjectAddress ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode);
+static ObjectAddress ATExecDropExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
+ bool missing_ok, LOCKMODE lockmode);
static ObjectAddress ATExecSetStatistics(Relation rel, const char *colName, int16 colNum,
Node *newValue, LOCKMODE lockmode);
static ObjectAddress ATExecSetOptions(Relation rel, const char *colName,
@@ -5381,7 +5382,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
address = ATExecSetExpression(tab, rel, cmd->name, cmd->def, lockmode);
break;
case AT_DropExpression:
- address = ATExecDropExpression(rel, cmd->name, cmd->missing_ok, lockmode);
+ address = ATExecDropExpression(tab, rel, cmd->name, cmd->missing_ok, lockmode);
break;
case AT_SetStatistics: /* ALTER COLUMN SET STATISTICS */
address = ATExecSetStatistics(rel, cmd->name, cmd->num, cmd->def, lockmode);
@@ -8658,7 +8659,8 @@ ATPrepDropExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recurs
* Return the address of the affected column.
*/
static ObjectAddress
-ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode)
+ATExecDropExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
+ bool missing_ok, LOCKMODE lockmode)
{
HeapTuple tuple;
Form_pg_attribute attTup;
@@ -8684,19 +8686,6 @@ ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMOD
errmsg("cannot alter system column \"%s\"",
colName)));
- /*
- * TODO: This could be done, but it would need a table rewrite to
- * materialize the generated values. Note that for the time being, we
- * still error with missing_ok, so that we don't silently leave the column
- * as generated.
- */
- if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("ALTER TABLE / DROP EXPRESSION is not supported for virtual generated columns"),
- errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
- colName, RelationGetRelationName(rel))));
-
if (!attTup->attgenerated)
{
if (!missing_ok)
@@ -8715,6 +8704,21 @@ ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMOD
}
}
+ if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ {
+ NewColumnValue *newval;
+ Expr *defval;
+
+ newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue));
+ newval->attnum = attnum;
+ defval = (Expr *) build_generation_expression(rel, attnum);
+ newval->expr = defval;
+ newval->is_generated = true;
+
+ tab->newvals = lappend(tab->newvals, newval);
+ tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+ }
+
/*
* Mark the column as no longer generated. (The atthasdef flag needs to
* get cleared too, but RemoveAttrDefault will handle that.)
diff --git a/src/test/regress/expected/fast_default.out b/src/test/regress/expected/fast_default.out
index ccbcdf8403f..7e99b822f11 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -70,6 +70,9 @@ NOTICE: rewriting table has_volatile for reason 4
-- stored generated columns need a rewrite
ALTER TABLE has_volatile ADD col7 int GENERATED ALWAYS AS (55) stored;
NOTICE: rewriting table has_volatile for reason 2
+-- drop generation expression over virtual generated colum need rewrite
+ALTER TABLE has_volatile ALTER COLUMN col6 DROP EXPRESSION;
+NOTICE: rewriting table has_volatile for reason 2
-- Test a large sample of different datatypes
CREATE TABLE T(pk INT NOT NULL PRIMARY KEY, c_int INT DEFAULT 1);
SELECT set('t');
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index dc09c85938e..959cf4608ab 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1133,27 +1133,24 @@ SELECT * FROM gtest29;
a | integer | | |
b | integer | | | generated always as (a * 3)
-ALTER TABLE gtest29 ALTER COLUMN b DROP EXPRESSION; -- not supported
-ERROR: ALTER TABLE / DROP EXPRESSION is not supported for virtual generated columns
-DETAIL: Column "b" of relation "gtest29" is a virtual generated column.
+ALTER TABLE gtest29 ALTER COLUMN b DROP EXPRESSION;
INSERT INTO gtest29 (a) VALUES (5);
INSERT INTO gtest29 (a, b) VALUES (6, 66);
-ERROR: cannot insert a non-DEFAULT value into column "b"
-DETAIL: Column "b" is a generated column.
-SELECT * FROM gtest29;
+SELECT * FROM gtest29 ORDER BY a, b;
a | b
---+----
3 | 9
4 | 12
- 5 | 15
-(3 rows)
+ 5 |
+ 6 | 66
+(4 rows)
\d gtest29
- Table "generated_virtual_tests.gtest29"
- Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+-----------------------------
+ Table "generated_virtual_tests.gtest29"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
a | integer | | |
- b | integer | | | generated always as (a * 3)
+ b | integer | | |
-- check that dependencies between columns have also been removed
--ALTER TABLE gtest29 DROP COLUMN a; -- should not drop b
@@ -1165,22 +1162,20 @@ CREATE TABLE gtest30 (
);
CREATE TABLE gtest30_1 () INHERITS (gtest30);
ALTER TABLE gtest30 ALTER COLUMN b DROP EXPRESSION;
-ERROR: ALTER TABLE / DROP EXPRESSION is not supported for virtual generated columns
-DETAIL: Column "b" of relation "gtest30" is a virtual generated column.
\d gtest30
- Table "generated_virtual_tests.gtest30"
- Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+-----------------------------
+ Table "generated_virtual_tests.gtest30"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
a | integer | | |
- b | integer | | | generated always as (a * 2)
+ b | integer | | |
Number of child tables: 1 (Use \d+ to list them.)
\d gtest30_1
- Table "generated_virtual_tests.gtest30_1"
- Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+-----------------------------
+ Table "generated_virtual_tests.gtest30_1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
a | integer | | |
- b | integer | | | generated always as (a * 2)
+ b | integer | | |
Inherits: gtest30
DROP TABLE gtest30 CASCADE;
@@ -1210,6 +1205,48 @@ Inherits: gtest30
ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION; -- error
ERROR: cannot drop generation expression from inherited column
+-- alter table drop expression with partitioning
+CREATE TABLE gtest_pp (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE (f1);
+CREATE TABLE gtest_ch1 PARTITION OF gtest_pp
+ FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- inherits gen expr
+CREATE TABLE gtest_ch2 PARTITION OF gtest_pp (
+ f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 22) VIRTUAL -- overrides gen expr
+) FOR VALUES FROM ('2016-08-01') TO ('2016-09-01');
+INSERT INTO gtest_pp (f1, f2) VALUES ('2016-07-15', 1), ('2016-07-15', 2), ('2016-08-15', 3);
+ALTER TABLE ONLY gtest_pp ALTER COLUMN f3 SET EXPRESSION AS (f2 * 4);
+ALTER TABLE gtest_ch1 ALTER COLUMN f3 SET EXPRESSION AS (f2 * 10);
+SELECT tableoid::regclass, * FROM gtest_pp ORDER BY 1, 2, 3;
+ tableoid | f1 | f2 | f3
+-----------+------------+----+----
+ gtest_ch1 | 07-15-2016 | 1 | 4
+ gtest_ch1 | 07-15-2016 | 2 | 8
+ gtest_ch2 | 08-15-2016 | 3 | 12
+(3 rows)
+
+ALTER TABLE gtest_ch1 ALTER COLUMN f3 DROP EXPRESSION; --error
+ERROR: cannot drop generation expression from inherited column
+ALTER TABLE ONLY gtest_pp ALTER COLUMN f3 DROP EXPRESSION; --error
+ERROR: ALTER TABLE / DROP EXPRESSION must be applied to child tables too
+ALTER TABLE gtest_pp ALTER COLUMN f3 DROP EXPRESSION; --ok
+SELECT tableoid::regclass, * FROM gtest_pp ORDER BY 1, 2, 3;
+ tableoid | f1 | f2 | f3
+-----------+------------+----+----
+ gtest_ch1 | 07-15-2016 | 1 | 10
+ gtest_ch1 | 07-15-2016 | 2 | 20
+ gtest_ch2 | 08-15-2016 | 3 | 66
+(3 rows)
+
+\d gtest_pp
+Partitioned table "generated_virtual_tests.gtest_pp"
+ Column | Type | Collation | Nullable | Default
+--------+--------+-----------+----------+---------
+ f1 | date | | not null |
+ f2 | bigint | | |
+ f3 | bigint | | |
+Partition key: RANGE (f1)
+Number of partitions: 2 (Use \d+ to list them.)
+
+DROP TABLE gtest_pp;
-- composite type dependencies
CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') VIRTUAL, c text);
CREATE TABLE gtest31_2 (x int, y gtest31_1);
diff --git a/src/test/regress/sql/fast_default.sql b/src/test/regress/sql/fast_default.sql
index 068dd0bc8aa..c0ef10b1ca3 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -77,6 +77,8 @@ ALTER TABLE has_volatile ALTER COLUMN col1 SET DATA TYPE float8,
-- stored generated columns need a rewrite
ALTER TABLE has_volatile ADD col7 int GENERATED ALWAYS AS (55) stored;
+-- drop generation expression over virtual generated colum need rewrite
+ALTER TABLE has_volatile ALTER COLUMN col6 DROP EXPRESSION;
-- Test a large sample of different datatypes
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index dab8c92ef99..d278a868107 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -562,10 +562,10 @@ ALTER TABLE gtest29 ALTER COLUMN b SET EXPRESSION AS (a * 3);
SELECT * FROM gtest29;
\d gtest29
-ALTER TABLE gtest29 ALTER COLUMN b DROP EXPRESSION; -- not supported
+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;
+SELECT * FROM gtest29 ORDER BY a, b;
\d gtest29
-- check that dependencies between columns have also been removed
@@ -592,6 +592,28 @@ ALTER TABLE ONLY gtest30 ALTER COLUMN b DROP EXPRESSION; -- error
\d gtest30_1
ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION; -- error
+-- alter table drop expression with partitioning
+CREATE TABLE gtest_pp (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE (f1);
+CREATE TABLE gtest_ch1 PARTITION OF gtest_pp
+ FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- inherits gen expr
+CREATE TABLE gtest_ch2 PARTITION OF gtest_pp (
+ f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 22) VIRTUAL -- overrides gen expr
+) FOR VALUES FROM ('2016-08-01') TO ('2016-09-01');
+INSERT INTO gtest_pp (f1, f2) VALUES ('2016-07-15', 1), ('2016-07-15', 2), ('2016-08-15', 3);
+
+ALTER TABLE ONLY gtest_pp ALTER COLUMN f3 SET EXPRESSION AS (f2 * 4);
+ALTER TABLE gtest_ch1 ALTER COLUMN f3 SET EXPRESSION AS (f2 * 10);
+
+SELECT tableoid::regclass, * FROM gtest_pp ORDER BY 1, 2, 3;
+
+ALTER TABLE gtest_ch1 ALTER COLUMN f3 DROP EXPRESSION; --error
+ALTER TABLE ONLY gtest_pp ALTER COLUMN f3 DROP EXPRESSION; --error
+
+ALTER TABLE gtest_pp ALTER COLUMN f3 DROP EXPRESSION; --ok
+SELECT tableoid::regclass, * FROM gtest_pp ORDER BY 1, 2, 3;
+\d gtest_pp
+DROP TABLE gtest_pp;
+
-- composite type dependencies
CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') VIRTUAL, c text);
CREATE TABLE gtest31_2 (x int, y gtest31_1);
--
2.34.1
jian he <jian.universality@gmail.com> writes:
the attached patch is to implement $subject.
Why would this be a good idea? I don't see any principled fallback
definition of the column. (No, "NULL" is not that.) Certainly we
should support ALTER TABLE DROP COLUMN, but removing the expression
and not providing a substitute seems semantically nonsensical.
regards, tom lane
On Wednesday, March 26, 2025, Tom Lane <tgl@sss.pgh.pa.us> wrote:
jian he <jian.universality@gmail.com> writes:
the attached patch is to implement $subject.
Why would this be a good idea? I don't see any principled fallback
definition of the column. (No, "NULL" is not that.) Certainly we
should support ALTER TABLE DROP COLUMN, but removing the expression
and not providing a substitute seems semantically nonsensical.
I don’t follow how NULL got involved in this discussion. The proposal is
basically: turn the virtual expression into an equivalent stored
expression, then drop the expression.
I suppose it would make sense to first add an alter table command to allow
the user to do a virtual/stored mode swap manually before adding this,
which then just becomes a convenient way to specify swap-and-drop as a
single command.
David J.
On Thu, Mar 27, 2025 at 11:44 AM David G. Johnston
<david.g.johnston@gmail.com> wrote:
On Wednesday, March 26, 2025, Tom Lane <tgl@sss.pgh.pa.us> wrote:
jian he <jian.universality@gmail.com> writes:
the attached patch is to implement $subject.
Why would this be a good idea? I don't see any principled fallback
definition of the column. (No, "NULL" is not that.) Certainly we
should support ALTER TABLE DROP COLUMN, but removing the expression
and not providing a substitute seems semantically nonsensical.I don’t follow how NULL got involved in this discussion. The proposal is basically: turn the virtual expression into an equivalent stored expression, then drop the expression.
This discussion [0]/messages/by-id/CAFCRh-8grTkEy+73q79iXB+q+sD=Qkbz-vNAN1KJCF6PRLa=zg@mail.gmail.com wants to change stored to virtual.
drop the virtual generation, not materialize, column value will be NULL,
but that will not work because commit [1]https://git.postgresql.org/cgit/postgresql.git/commit/?id=cdc168ad4b22ea4183f966688b245cabb5935d1f
So we are either saying that
virtual generation expression cannot be dropped, you can only
substitute another expression
or drop the expression, based on the dropped expression materializing
that column value.
[0]: /messages/by-id/CAFCRh-8grTkEy+73q79iXB+q+sD=Qkbz-vNAN1KJCF6PRLa=zg@mail.gmail.com
[1]: https://git.postgresql.org/cgit/postgresql.git/commit/?id=cdc168ad4b22ea4183f966688b245cabb5935d1f
On Wed, Mar 26, 2025 at 8:15 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
jian he <jian.universality@gmail.com> writes:
the attached patch is to implement $subject.
Why would this be a good idea? I don't see any principled fallback
definition of the column. (No, "NULL" is not that.) Certainly we
should support ALTER TABLE DROP COLUMN, but removing the expression
and not providing a substitute seems semantically nonsensical.
The fallback value being proposed is the result of evaluating the
about-to-be-dropped expression.
We already allow removing a generated expression from a column so it cannot
be that nonsensical.
In either case we are saying the value of this column for a given row is
X. If you "select col from tbl where id = n" you will get "X".
Whether X is:
Physical
Stored Generated
Virtual Generated
is immaterial.
Physical - Physical: N/A
Physical - Stored: Disallowed (syntax but doesn't work)
Physical - Virtual: Disallowed (no syntax)
Stored - Physical: Drop Expression (no table rewrite)
Stored - Stored: Set Expression As (table rewrite)
Stored - Virtual: Disallowed (no syntax)
*Virtual - Physical: Prohibited; Proposal: Drop Expression (table rewrite)*
Virtual - Stored: Disallowed (no syntax)
Virtual - Virtual: Set Expression As (no table rewrite)
In short, the following returns '1id' today.
create table tbl (id serial primary key,
val text not null generated always as (id || 'id') stored);
insert into tbl values (default, default);
alter table tbl alter val drop expression;
select * from tbl;
This otherwise identical sequence (just using virtual) returns "not
implemented", and this proposal means to implement it.
create table tbl (id serial primary key,
val text not null generated always as (id || 'id') virtual);
insert into tbl values (default, default);
alter table tbl alter val drop expression;
select * from tbl; -- would return '1id' under the proposal (not tested...)
The reference to 'NULL' is because the physical table has no stored value
of '1id' and so we need a table rewrite to populate it.
David J.