Subject: Re: Virtual generated columns

From: Dean Rasheed <[email-masked]>
Date: Thu, 8 Aug 2024 19:22:28 +0100

On Thu, 8 Aug 2024 at 07:23, Peter Eisentraut <[email-masked]> wrote:
>
> Thank you for your extensive testing.  Here is a new patch set that has
> fixed all the issues you have reported (MERGE, sublinks, statistics,
> ANALYZE).

I had a quick look at this and found one issue, which is that it
doesn't properly deal with virtual generated columns in wholerow
attributes:

CREATE TABLE foo(a int, a2 int GENERATED ALWAYS AS (a*2) VIRTUAL);
INSERT INTO foo VALUES (1);
SELECT foo FROM foo;

 foo
------
 (1,)
(1 row)

Looking at the rewriter changes, it occurred to me that it could
perhaps be done more simply using ReplaceVarsFromTargetList() for each
RTE with virtual generated columns. That function already has the
required wholerow handling code, so there'd be less code duplication.
I think it might be better to do this from within fireRIRrules(), just
after RLS policies are applied, so it wouldn't need to worry about
CTEs and sublink subqueries. That would also make the
hasGeneratedVirtual flags unnecessary, since we'd already only be
doing the extra work for tables with virtual generated columns. That
would eliminate possible bugs caused by failing to set those flags.

Regards,
Dean

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 21 Aug 2024 09:00:44 +0200

> required wholerow handling code, so there'd be less code duplication.

Hmm, I don't quite see how ReplaceVarsFromTargetList() could be used 
here.  It does have the wholerow logic that we need somehow, but other 
than that it seems to target something different?

> would eliminate possible bugs caused by failing to set those flags.

Yes, ideally, we'd piggy-back this into fireRIRrules().  One thing I'm 
missing is that if you're descending into subqueries, there is no link 
to the upper levels' range tables, which we need to lookup the 
pg_attribute entries of column referencing Vars.  That's why there is 
this whole custom walk with its own context data.  Maybe there is a way 
to do this already that I missed?

---

From: Dean Rasheed <[email-masked]>
Date: Wed, 21 Aug 2024 11:51:59 +0100

> than that it seems to target something different?

Well what I was thinking was that (in fireRIRrules()'s final loop over
relations in the rtable), if the relation had any virtual generated
columns, you'd build a targetlist containing a TLE for each one,
containing the generated expression. Then you could just call
ReplaceVarsFromTargetList() to replace any Vars in the query with the
corresponding generated expressions. That takes care of descending
into subqueries, adjusting varlevelsup, and expanding wholerow Vars
that might refer to the generated expression.

I also have half an eye on how this patch will interact with my patch
to support RETURNING OLD/NEW values. If you use
ReplaceVarsFromTargetList(), it should just do the right thing for
RETURNING OLD/NEW generated expressions.

> to do this already that I missed?

That link to the upper levels' range tables wouldn't be needed because
essentially using ReplaceVarsFromTargetList() flips the whole thing
round: instead of traversing the tree looking for Var nodes that need
to be replaced (possibly from upper query levels), you build a list of
replacement expressions to be applied and apply them from the top,
descending into subqueries as needed.

Another argument for doing it that way round is to not add too many
extra cycles to the processing of existing queries that don't
reference generated expressions. ISTM that this patch is potentially
adding quite a lot of additional overhead -- it looks like, for every
Var in the tree, it's calling get_attgenerated(), which involves a
syscache lookup to see if that column is a generated expression (which
most won't be). Ideally, we should be trying to do the minimum amount
of extra work in the common case where there are no generated
expressions.

Looking ahead, I can also imagine that one day we might want to
support subqueries in generated expressions. That would require
recursive processing of generated expressions in the generated
expression's subquery, as well as applying RLS policies to the new
relations pulled in, and checks to guard against infinite recursion.
fireRIRrules() already has the infrastructure to support all of that,
so that feels like a much more natural place to do this.

Regards,
Dean

---

From: jian he <[email-masked]>
Date: Fri, 23 Aug 2024 17:06:00 +0800

drop table if exists gtest_err_1 cascade;
CREATE TABLE gtest_err_1 (
a int PRIMARY KEY generated by default as identity,
b int GENERATED ALWAYS AS (22),
d int default 22);
create view gtest_err_1_v as select * from gtest_err_1;
SELECT events & 4 != 0 AS can_upd, events & 8 != 0 AS can_ins,events &
16 != 0 AS can_del
FROM pg_catalog.pg_relation_is_updatable('gtest_err_1_v'::regclass,
false) t(events);

insert into gtest_err_1_v(a,b, d) values ( 11, default,33) returning *;
should the above query, b return 22?
even b is  "b int default" will return 22.

drop table if exists comment_test cascade;
CREATE TABLE comment_test (
  id int,
  positive_col int  GENERATED ALWAYS AS (22) CHECK (positive_col > 0),
  positive_col1 int  GENERATED ALWAYS AS (22) stored CHECK (positive_col > 0) ,
  indexed_col int,
  CONSTRAINT comment_test_pk PRIMARY KEY (id));
CREATE INDEX comment_test_index ON comment_test(indexed_col);
ALTER TABLE comment_test ALTER COLUMN positive_col1 SET DATA TYPE text;
ALTER TABLE comment_test ALTER COLUMN positive_col SET DATA TYPE text;
the last query should work just fine?

drop table if exists def_test cascade;
create table def_test (
    c0    int4 GENERATED ALWAYS AS (22) stored,
    c1    int4 GENERATED ALWAYS AS (22),
    c2    text default 'initial_default'
);
alter table def_test alter column c1 set default 10;
ERROR:  column "c1" of relation "def_test" is a generated column
HINT:  Use ALTER TABLE ... ALTER COLUMN ... SET EXPRESSION instead.
alter table def_test alter column c1 drop default;
ERROR:  column "c1" of relation "def_test" is a generated column

Is the first error message hint wrong?
also the second error message (column x is a generated column) is not helpful.
here, we should just say that cannot set/drop default for virtual
generated column?

drop table if exists bar1, bar2;
create table bar1(a integer, b integer GENERATED ALWAYS AS (22))
partition by range (a);
create table bar2(a integer);
alter table bar2 add column b integer GENERATED ALWAYS AS (22) stored;
alter table bar1 attach partition bar2 default;
this works, which will make partitioned table and partition have
different kinds of generated column,
but this is not what we expected?

another variant:
CREATE TABLE list_parted (
a int NOT NULL,
b char(2) COLLATE "C",
c int GENERATED ALWAYS AS (22)
) PARTITION BY LIST (a);
CREATE TABLE parent (LIKE list_parted);
ALTER TABLE parent drop column c, add column c int GENERATED ALWAYS AS
(22) stored;
ALTER TABLE list_parted ATTACH PARTITION parent FOR VALUES IN (1);

drop table if exists tp, tpp1, tpp2;
CREATE TABLE tp (a int NOT NULL,b text GENERATED ALWAYS AS (22),c
text) PARTITION BY LIST (a);
CREATE TABLE tpp1(a int NOT NULL, b text GENERATED ALWAYS AS (c
||'1000' ), c text);
ALTER TABLE tp ATTACH PARTITION tpp1 FOR VALUES IN (1);
insert into tp(a,b,c) values (1,default, 'hello') returning a,b,c;
insert into tpp1(a,b,c) values (1,default, 'hello') returning a,b,c;

select tableoid::regclass, * from tpp1;
select tableoid::regclass, * from tp;
the above two queries return different results, slightly unintuitive, i guess.
Do we need to mention it somewhere?

CREATE TABLE atnotnull1 ();
ALTER TABLE atnotnull1 ADD COLUMN c INT GENERATED ALWAYS AS (22), ADD
PRIMARY KEY (c);
ERROR:  not-null constraints are not supported on virtual generated columns
DETAIL:  Column "c" of relation "atnotnull1" is a virtual generated column.
I guess this error message is fine.

The last issue in the previous thread [1], ATPrepAlterColumnType
seems not addressed.

[1] https://postgr.es/m/[email-masked]

---

From: jian he <[email-masked]>
Date: Thu, 29 Aug 2024 17:01:00 +0800

> so that feels like a much more natural place to do this.

Is the attached something you are thinking of?
(mainly see changes of src/backend/rewrite/rewriteHandler.c)

i bloated rewriteHandler.c a lot, mainly because
expand_generated_columns_in_expr
not using ReplaceVarsFromTargetList, only expand_generated_columns_in_query do.

if we are using ReplaceVarsFromTargetList, then
expand_generated_columns_in_expr also needs to use ReplaceVarsFromTargetList?

I don't think we can call ReplaceVarsFromTargetList within
expand_generated_columns_in_expr.

if so, then the pattern would be like:
{
            Node       *tgqual;
            tgqual = (Node *) expand_generated_columns_in_expr(tgqual,
relinfo->ri_RelationDesc, context);
           ReplaceVarsFromTargetList
}

There are 6 of expand_generated_columns_in_expr called.

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 29 Aug 2024 14:15:50 +0200

> even b is  "b int default" will return 22.

Confirmed.  This is a bug in the rewriting that will hopefully be fixed 
when I get to, uh, rewriting that using Dean's suggestions.  Not done 
here.  (The problem, in the current implementation, is that 
query->hasGeneratedVirtual does not get preserved in the view, and then 
expand_generated_columns_in_query() does the wrong thing.)

> the last query should work just fine?

I played with this and I don't see anything wrong with the current 
behavior.  I noticed that in your test case

 >    positive_col1 int  GENERATED ALWAYS AS (22) stored CHECK 
(positive_col > 0) ,

you have the wrong column name in the check constraint.  I'm not sure if 
that was intentional.

> Is the first error message hint wrong?

Yes, somewhat.  I looked into fixing that, but that got a bit messy.  I 
hope to be able to implement SET EXPRESSION before too long, so I'm 
leaving it for now.

> here, we should just say that cannot set/drop default for virtual

Maybe, but that's not part of this patch.

> but this is not what we expected?

Fixed.  (Needed code in MergeAttributesIntoExisting() similar to 
MergeChildAttribute().)

> Do we need to mention it somewhere?

It is documented in ddl.sgml:

+ For virtual
+ generated columns, the generation expression of the table named in the
+ query applies when a table is read.

> I guess this error message is fine.

Yeah, maybe this will get improved when the catalogued not-null 
constraints come back.  Better wait for that.

> [1] https://postgr.es/m/[email-masked]

This is fixed now.

I also committed the two patches that renamed the existing test files, 
so those are not included here anymore.

The new patch does some rebasing and contains various fixes to the 
issues you presented.  As I mentioned, I'll look into improving the 
rewriting.

From 5a73fb029cc215134d0213c06ede10a3a42b755b Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Thu, 29 Aug 2024 11:49:48 +0200
Subject: [PATCH v4] Virtual generated columns

This adds a new variant of generated columns that are computed on read
(like a view, unlike the existing stored generated columns, which are
computed on write, like a materialized view).

Some functionality is currently not supported (but could possibly be
added as incremental features, some easier than others):

- index on virtual column
- expression index using a virtual column
- hence also no unique constraints on virtual columns
- not-null constraints on virtual columns
- (check constraints are supported)
- foreign key constraints on virtual columns
- extended statistics on virtual columns
- ALTER TABLE / SET EXPRESSION
- ALTER TABLE / DROP EXPRESSION
- virtual columns as trigger columns
- virtual column cannot have domain type

TODO:
- analysis of access control
- check FDW/foreign table behavior

Discussion: https://www.postgresql.org/message-id/flat/[email-masked]
---
 contrib/pageinspect/expected/page.out         |  35 +
 contrib/pageinspect/sql/page.sql              |  17 +
 .../postgres_fdw/expected/postgres_fdw.out    |  81 +-
 contrib/postgres_fdw/sql/postgres_fdw.sql     |   9 +-
 doc/src/sgml/catalogs.sgml                    |   4 +-
 doc/src/sgml/ddl.sgml                         |  25 +-
 doc/src/sgml/ref/alter_table.sgml             |  14 +-
 doc/src/sgml/ref/create_foreign_table.sgml    |  11 +-
 doc/src/sgml/ref/create_table.sgml            |  22 +-
 doc/src/sgml/ref/create_trigger.sgml          |   2 +-
 doc/src/sgml/trigger.sgml                     |   4 +
 src/backend/access/common/tupdesc.c           |   3 +
 src/backend/access/heap/heapam_handler.c      |   2 +
 src/backend/catalog/heap.c                    |  13 +-
 src/backend/commands/analyze.c                |   4 +
 src/backend/commands/indexcmds.c              |  29 +-
 src/backend/commands/statscmds.c              |  20 +-
 src/backend/commands/tablecmds.c              | 124 ++-
 src/backend/commands/trigger.c                |  48 +-
 src/backend/executor/execExprInterp.c         |   4 +
 src/backend/executor/execMain.c               |   5 +-
 src/backend/parser/analyze.c                  |   8 +
 src/backend/parser/gram.y                     |  15 +-
 src/backend/parser/parse_clause.c             |   4 +
 src/backend/parser/parse_merge.c              |   1 +
 src/backend/parser/parse_relation.c           |   9 +-
 src/backend/parser/parse_utilcmd.c            |  14 +-
 src/backend/replication/pgoutput/pgoutput.c   |   3 +-
 src/backend/rewrite/rewriteHandler.c          | 171 +++-
 src/backend/utils/cache/partcache.c           |   3 +
 src/backend/utils/cache/relcache.c            |   2 +
 src/bin/pg_dump/pg_dump.c                     |   3 +
 src/bin/pg_dump/t/002_pg_dump.pl              |   6 +-
 src/bin/psql/describe.c                       |   6 +
 src/include/access/tupdesc.h                  |   1 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/pg_attribute.h            |   1 +
 src/include/nodes/parsenodes.h                |   3 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/parser/parse_node.h               |   1 +
 src/include/rewrite/rewriteHandler.h          |   2 +
 src/pl/plperl/expected/plperl_trigger.out     |   7 +-
 src/pl/plperl/plperl.c                        |   3 +
 src/pl/plperl/sql/plperl_trigger.sql          |   3 +-
 src/pl/plpython/expected/plpython_trigger.out |   7 +-
 src/pl/plpython/plpy_typeio.c                 |   3 +
 src/pl/plpython/sql/plpython_trigger.sql      |   3 +-
 src/pl/tcl/expected/pltcl_trigger.out         |  19 +-
 src/pl/tcl/pltcl.c                            |   3 +
 src/pl/tcl/sql/pltcl_trigger.sql              |   3 +-
 .../regress/expected/create_table_like.out    |  23 +-
 .../regress/expected/generated_stored.out     |  32 +-
 ...rated_stored.out => generated_virtual.out} | 871 +++++++++---------
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/sql/create_table_like.sql    |   2 +-
 src/test/regress/sql/generated_stored.sql     |  14 +-
 ...rated_stored.sql => generated_virtual.sql} | 336 ++++---
 src/test/subscription/t/011_generated.pl      |  38 +-
 58 files changed, 1391 insertions(+), 710 deletions(-)
 copy src/test/regress/expected/{generated_stored.out => generated_virtual.out} (67%)
 copy src/test/regress/sql/{generated_stored.sql => generated_virtual.sql} (69%)

diff --git a/contrib/pageinspect/expected/page.out b/contrib/pageinspect/expected/page.out
index 80ddb45a60a..7e0b09e279b 100644
--- a/contrib/pageinspect/expected/page.out
+++ b/contrib/pageinspect/expected/page.out
@@ -208,6 +208,41 @@ select tuple_data_split('test8'::regclass, t_data, t_infomask, t_infomask2, t_bi
 (1 row)
 
 drop table test8;
+-- check storage of generated columns
+-- stored
+create table test9s (a int not null, b int generated always as (a * 2) stored);
+insert into test9s values (131584);
+select t_infomask, t_bits, t_data from heap_page_items(get_raw_page('test9s', 0));
+ t_infomask | t_bits |       t_data       
+------------+--------+--------------------
+       2048 |        | \x0002020000040400
+(1 row)
+
+select tuple_data_split('test9s'::regclass, t_data, t_infomask, t_infomask2, t_bits)
+    from heap_page_items(get_raw_page('test9s', 0));
+       tuple_data_split        
+-------------------------------
+ {"\\x00020200","\\x00040400"}
+(1 row)
+
+drop table test9s;
+-- virtual
+create table test9v (a int not null, b int generated always as (a * 2) virtual);
+insert into test9v values (131584);
+select t_infomask, t_bits, t_data from heap_page_items(get_raw_page('test9v', 0));
+ t_infomask |  t_bits  |   t_data   
+------------+----------+------------
+       2049 | 10000000 | \x00020200
+(1 row)
+
+select tuple_data_split('test9v'::regclass, t_data, t_infomask, t_infomask2, t_bits)
+    from heap_page_items(get_raw_page('test9v', 0));
+   tuple_data_split   
+----------------------
+ {"\\x00020200",NULL}
+(1 row)
+
+drop table test9v;
 -- Failure with incorrect page size
 -- Suppress the DETAIL message, to allow the tests to work across various
 -- page sizes.
diff --git a/contrib/pageinspect/sql/page.sql b/contrib/pageinspect/sql/page.sql
index 5bff568d3b5..186dda1e8d0 100644
--- a/contrib/pageinspect/sql/page.sql
+++ b/contrib/pageinspect/sql/page.sql
@@ -84,6 +84,23 @@ CREATE TEMP TABLE test1 (a int, b int);
     from heap_page_items(get_raw_page('test8', 0));
 drop table test8;
 
+-- check storage of generated columns
+-- stored
+create table test9s (a int not null, b int generated always as (a * 2) stored);
+insert into test9s values (131584);
+select t_infomask, t_bits, t_data from heap_page_items(get_raw_page('test9s', 0));
+select tuple_data_split('test9s'::regclass, t_data, t_infomask, t_infomask2, t_bits)
+    from heap_page_items(get_raw_page('test9s', 0));
+drop table test9s;
+
+-- virtual
+create table test9v (a int not null, b int generated always as (a * 2) virtual);
+insert into test9v values (131584);
+select t_infomask, t_bits, t_data from heap_page_items(get_raw_page('test9v', 0));
+select tuple_data_split('test9v'::regclass, t_data, t_infomask, t_infomask2, t_bits)
+    from heap_page_items(get_raw_page('test9v', 0));
+drop table test9v;
+
 -- Failure with incorrect page size
 -- Suppress the DETAIL message, to allow the tests to work across various
 -- page sizes.
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index f3eb055e2c7..926c1fddbe3 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7283,65 +7283,68 @@ select * from rem1;
 -- ===================================================================
 create table gloc1 (
   a int,
-  b int generated always as (a * 2) stored);
+  b int generated always as (a * 2) stored,
+  c int
+);
 alter table gloc1 set (autovacuum_enabled = 'false');
 create foreign table grem1 (
   a int,
-  b int generated always as (a * 2) stored)
-  server loopback options(table_name 'gloc1');
+  b int generated always as (a * 2) stored,
+  c int generated always as (a * 3) virtual
+) server loopback options(table_name 'gloc1');
 explain (verbose, costs off)
 insert into grem1 (a) values (1), (2);
-                            QUERY PLAN                             
--------------------------------------------------------------------
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
  Insert on public.grem1
-   Remote SQL: INSERT INTO public.gloc1(a, b) VALUES ($1, DEFAULT)
+   Remote SQL: INSERT INTO public.gloc1(a, b, c) VALUES ($1, DEFAULT, DEFAULT)
    Batch Size: 1
    ->  Values Scan on "*VALUES*"
-         Output: "*VALUES*".column1, NULL::integer
+         Output: "*VALUES*".column1, NULL::integer, NULL::integer
 (5 rows)
 
 insert into grem1 (a) values (1), (2);
 explain (verbose, costs off)
 update grem1 set a = 22 where a = 2;
-                                     QUERY PLAN                                     
-------------------------------------------------------------------------------------
+                                       QUERY PLAN                                       
+----------------------------------------------------------------------------------------
  Update on public.grem1
-   Remote SQL: UPDATE public.gloc1 SET a = $2, b = DEFAULT WHERE ctid = $1
+   Remote SQL: UPDATE public.gloc1 SET a = $2, b = DEFAULT, c = DEFAULT WHERE ctid = $1
    ->  Foreign Scan on public.grem1
          Output: 22, ctid, grem1.*
-         Remote SQL: SELECT a, b, ctid FROM public.gloc1 WHERE ((a = 2)) FOR UPDATE
+         Remote SQL: SELECT a, b, c, ctid FROM public.gloc1 WHERE ((a = 2)) FOR UPDATE
 (5 rows)
 
 update grem1 set a = 22 where a = 2;
 select * from gloc1;
- a  | b  
-----+----
-  1 |  2
- 22 | 44
+ a  | b  | c 
+----+----+---
+  1 |  2 |  
+ 22 | 44 |  
 (2 rows)
 
 select * from grem1;
- a  | b  
-----+----
-  1 |  2
- 22 | 44
+ a  | b  | c  
+----+----+----
+  1 |  2 |  3
+ 22 | 44 | 66
 (2 rows)
 
 delete from grem1;
 -- test copy from
 copy grem1 from stdin;
 select * from gloc1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 |  
+ 2 | 4 |  
 (2 rows)
 
 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;
@@ -7349,28 +7352,28 @@ delete from grem1;
 alter server loopback options (add batch_size '10');
 explain (verbose, costs off)
 insert into grem1 (a) values (1), (2);
-                            QUERY PLAN                             
--------------------------------------------------------------------
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
  Insert on public.grem1
-   Remote SQL: INSERT INTO public.gloc1(a, b) VALUES ($1, DEFAULT)
+   Remote SQL: INSERT INTO public.gloc1(a, b, c) VALUES ($1, DEFAULT, DEFAULT)
    Batch Size: 10
    ->  Values Scan on "*VALUES*"
-         Output: "*VALUES*".column1, NULL::integer
+         Output: "*VALUES*".column1, NULL::integer, NULL::integer
 (5 rows)
 
 insert into grem1 (a) values (1), (2);
 select * from gloc1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 |  
+ 2 | 4 |  
 (2 rows)
 
 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 0734716ad90..058d53364db 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -1836,12 +1836,15 @@ CREATE VIEW rw_view AS SELECT * FROM parent_tbl WHERE a < 5 WITH CHECK OPTION;
 -- ===================================================================
 create table gloc1 (
   a int,
-  b int generated always as (a * 2) stored);
+  b int generated always as (a * 2) stored,
+  c int
+);
 alter table gloc1 set (autovacuum_enabled = 'false');
 create foreign table grem1 (
   a int,
-  b int generated always as (a * 2) stored)
-  server loopback options(table_name 'gloc1');
+  b int generated always as (a * 2) stored,
+  c int generated always as (a * 3) virtual
+) server loopback options(table_name 'gloc1');
 explain (verbose, costs off)
 insert into grem1 (a) values (1), (2);
 insert into grem1 (a) values (1), (2);
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index b654fae1b2f..579b1d61660 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1318,8 +1318,8 @@ <title><structname>pg_attribute</structname> Columns</title>
       </para>
       <para>
        If a zero byte (<literal>''</literal>), then not a generated column.
-       Otherwise, <literal>s</literal> = stored.  (Other values might be added
-       in the future.)
+       Otherwise, <literal>s</literal> = stored, <literal>v</literal> =
+       virtual.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index b671858627b..378ae934baa 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -361,7 +361,6 @@ <title>Generated Columns</title>
    storage and is computed when it is read.  Thus, a virtual generated column
    is similar to a view and a stored generated column is similar to a
    materialized view (except that it is always updated automatically).
-   <productname>PostgreSQL</productname> currently implements only stored generated columns.
   </para>
 
   <para>
@@ -371,12 +370,12 @@ <title>Generated Columns</title>
 CREATE TABLE people (
     ...,
     height_cm numeric,
-    height_in numeric <emphasis>GENERATED ALWAYS AS (height_cm / 2.54) STORED</emphasis>
+    height_in numeric <emphasis>GENERATED ALWAYS AS (height_cm / 2.54)</emphasis>
 );
 </programlisting>
-   The keyword <literal>STORED</literal> must be specified to choose the
-   stored kind of generated column.  See <xref linkend="sql-createtable"/> for
-   more details.
+   A generated column is by default of the virtual kind.  Use the keywords
+   <literal>VIRTUAL</literal> or <literal>STORED</literal> to make the choice
+   explicit.  See <xref linkend="sql-createtable"/> for more details.
   </para>
 
   <para>
@@ -442,12 +441,18 @@ <title>Generated Columns</title>
       <listitem>
        <para>
         If a parent column is a generated column, its child column must also
-        be a generated column; however, the child column can have a
-        different generation expression.  The generation expression that is
+        be a generated column of the same kind (stored or virtual); however,
+        the child column can have a different generation expression.
+       </para>
+
+       <para>
+        For stored generated columns, the generation expression that is
         actually applied during insert or update of a row is the one
-        associated with the table that the row is physically in.
-        (This is unlike the behavior for column defaults: for those, the
-        default value associated with the table named in the query applies.)
+        associated with the table that the row is physically in.  (This is
+        unlike the behavior for column defaults: for those, the default value
+        associated with the table named in the query applies.)  For virtual
+        generated columns, the generation expression of the table named in the
+        query applies when a table is read.
        </para>
       </listitem>
       <listitem>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 1a49f321cf7..c5aaf765446 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -264,6 +264,11 @@ <title>Description</title>
       in the column is rewritten and all the future changes will apply the new
       generation expression.
      </para>
+
+     <para>
+      This form is currently only supported for stored generated columns (not
+      virtual ones).
+     </para>
     </listitem>
    </varlistentry>
 
@@ -276,10 +281,15 @@ <title>Description</title>
       longer apply the generation expression.
      </para>
 
+     <para>
+      This form is currently only supported for stored generated columns (not
+      virtual ones).
+     </para>
+
      <para>
       If <literal>DROP EXPRESSION IF EXISTS</literal> is specified and the
-      column is not a stored generated column, no error is thrown.  In this
-      case a notice is issued instead.
+      column is not a generated column, no error is thrown.  In this case a
+      notice is issued instead.
      </para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/ref/create_foreign_table.sgml b/doc/src/sgml/ref/create_foreign_table.sgml
index dc4b9075990..21b5d6a14d0 100644
--- a/doc/src/sgml/ref/create_foreign_table.sgml
+++ b/doc/src/sgml/ref/create_foreign_table.sgml
@@ -47,7 +47,7 @@
   NULL |
   CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
   DEFAULT <replaceable>default_expr</replaceable> |
-  GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED }
+  GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) [ STORED | VIRTUAL ] }
 
 <phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
 
@@ -275,7 +275,7 @@ <title>Parameters</title>
    </varlistentry>
 
    <varlistentry>
-    <term><literal>GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED</literal><indexterm><primary>generated column</primary></indexterm></term>
+    <term><literal>GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) [ STORED | VIRTUAL ]</literal><indexterm><primary>generated column</primary></indexterm></term>
     <listitem>
      <para>
       This clause creates the column as a <firstterm>generated
@@ -284,10 +284,13 @@ <title>Parameters</title>
      </para>
 
      <para>
-      The keyword <literal>STORED</literal> is required to signify that the
+      When <literal>VIRTUAL</literal> is specified, the column will be
+      computed when it is read.  (The foreign-data wrapper will see it as a
+      null value in new rows and may choose to store it as a null value or
+      ignore it altogether.)  When <literal>STORED</literal> is specified, the
       column will be computed on write.  (The computed value will be presented
       to the foreign-data wrapper for storage and must be returned on
-      reading.)
+      reading.)  <literal>VIRTUAL</literal> is the default.
      </para>
 
      <para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 93b3f664f21..ee79867d969 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -65,7 +65,7 @@
   NULL |
   CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
   DEFAULT <replaceable>default_expr</replaceable> |
-  GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED |
+  GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) [ STORED | VIRTUAL ] |
   GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( <replaceable>sequence_options</replaceable> ) ] |
   UNIQUE [ NULLS [ NOT ] DISTINCT ] <replaceable class="parameter">index_parameters</replaceable> |
   PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
@@ -720,8 +720,9 @@ <title>Parameters</title>
         <term><literal>INCLUDING GENERATED</literal></term>
         <listitem>
          <para>
-          Any generation expressions of copied column definitions will be
-          copied.  By default, new columns will be regular base columns.
+          Any generation expressions as well as the stored/virtual choice of
+          copied column definitions will be copied.  By default, new columns
+          will be regular base columns.
          </para>
         </listitem>
        </varlistentry>
@@ -897,7 +898,7 @@ <title>Parameters</title>
    </varlistentry>
 
    <varlistentry id="sql-createtable-parms-generated-stored">
-    <term><literal>GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED</literal><indexterm><primary>generated column</primary></indexterm></term>
+    <term><literal>GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) [ STORED | VIRTUAL ]</literal><indexterm><primary>generated column</primary></indexterm></term>
     <listitem>
      <para>
       This clause creates the column as a <firstterm>generated
@@ -906,8 +907,11 @@ <title>Parameters</title>
      </para>
 
      <para>
-      The keyword <literal>STORED</literal> is required to signify that the
-      column will be computed on write and will be stored on disk.
+      When <literal>VIRTUAL</literal> is specified, the column will be
+      computed when it is read, and it will not occupy any storage.  When
+      <literal>STORED</literal> is specified, the column will be computed on
+      write and will be stored on disk.  <literal>VIRTUAL</literal> is the
+      default.
      </para>
 
      <para>
@@ -2380,9 +2384,9 @@ <title>Multiple Identity Columns</title>
    <title>Generated Columns</title>
 
    <para>
-    The option <literal>STORED</literal> is not standard but is also used by
-    other SQL implementations.  The SQL standard does not specify the storage
-    of generated columns.
+    The options <literal>STORED</literal> and <literal>VIRTUAL</literal> are
+    not standard but are also used by other SQL implementations.  The SQL
+    standard does not specify the storage of generated columns.
    </para>
   </refsect2>
 
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index 982ab6f3ee4..752fe50860a 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -279,7 +279,7 @@ <title>Parameters</title>
      <para>
       <literal>INSTEAD OF UPDATE</literal> events do not allow a list of columns.
       A column list cannot be specified when requesting transition relations,
-      either.
+      either.  Virtual generated columns are not supported in the column list.
      </para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index 31626536a2e..5953d24ae8f 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -289,6 +289,10 @@ <title>Overview of Trigger Behavior</title>
     <literal>BEFORE</literal> trigger.  Changes to the value of a generated
     column in a <literal>BEFORE</literal> trigger are ignored and will be
     overwritten.
+    Virtual generated columns are never computed when triggers fire.  In the C
+    language interface, their content is undefined in a trigger function.
+    Higher-level programming languages should prevent access to virtual
+    generated columns in triggers.
    </para>
 
    <para>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 47379fef220..13840ba533c 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -190,6 +190,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
 
 		cpy->has_not_null = constr->has_not_null;
 		cpy->has_generated_stored = constr->has_generated_stored;
+		cpy->has_generated_virtual = constr->has_generated_virtual;
 
 		if ((cpy->num_defval = constr->num_defval) > 0)
 		{
@@ -494,6 +495,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (constr1->has_generated_stored != constr2->has_generated_stored)
 			return false;
+		if (constr1->has_generated_virtual != constr2->has_generated_virtual)
+			return false;
 		n = constr1->num_defval;
 		if (n != (int) constr2->num_defval)
 			return false;
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 1c6da286d43..0c95a11f044 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -2048,6 +2048,8 @@ heapam_relation_needs_toast_table(Relation rel)
 
 		if (att->attisdropped)
 			continue;
+		if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			continue;
 		data_length = att_align_nominal(data_length, att->attalign);
 		if (att->attlen > 0)
 		{
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 01b43cc6a84..29358f37307 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -512,7 +512,7 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags | (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ? CHKATYPE_IS_VIRTUAL : 0));
 	}
 }
 
@@ -587,6 +587,17 @@ CheckAttributeType(const char *attname,
 	}
 	else if (att_typtype == TYPTYPE_DOMAIN)
 	{
+		/*
+		 * Prevent virtual generated columns from having a domain type.  We
+		 * would have to enforce domain constraints when columns underlying
+		 * the generated column change.  This could possibly be implemented,
+		 * but it's not.
+		 */
+		if (flags & CHKATYPE_IS_VIRTUAL)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("virtual generated column \"%s\" cannot have a domain type", attname)));
+
 		/*
 		 * If it's a domain, recurse to check its base type.
 		 */
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 902eb1a4508..f79ff40c3ec 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -1030,6 +1030,10 @@ examine_attribute(Relation onerel, int attnum, Node *index_expr)
 	if (attr->attisdropped)
 		return NULL;
 
+	/* Don't analyze virtual generated columns */
+	if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+		return NULL;
+
 	/*
 	 * Get attstattarget value.  Set to -1 if null.  (Analyze functions expect
 	 * -1 to mean use default_statistics_target; see for example
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index c5a56c75f69..ec4e8bc3902 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1085,6 +1085,9 @@ DefineIndex(Oid tableId,
 	/*
 	 * We disallow indexes on system columns.  They would not necessarily get
 	 * updated correctly, and they don't seem useful anyway.
+	 *
+	 * Also disallow virtual generated columns in indexes (use expression
+	 * index instead).
 	 */
 	for (int i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
 	{
@@ -1094,14 +1097,22 @@ DefineIndex(Oid tableId,
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("index creation on system columns is not supported")));
+
+
+		if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("index creation on virtual generated columns is not supported")));
 	}
 
 	/*
-	 * Also check for system columns used in expressions or predicates.
+	 * Also check for system and generated columns used in expressions or
+	 * predicates.
 	 */
 	if (indexInfo->ii_Expressions || indexInfo->ii_Predicate)
 	{
 		Bitmapset  *indexattrs = NULL;
+		int			j;
 
 		pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs);
 		pull_varattnos((Node *) indexInfo->ii_Predicate, 1, &indexattrs);
@@ -1114,6 +1125,22 @@ DefineIndex(Oid tableId,
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("index creation on system columns is not supported")));
 		}
+
+		/*
+		 * XXX Virtual generated columns in index expressions or predicates
+		 * could be supported, but it needs support in
+		 * RelationGetIndexExpressions() and RelationGetIndexPredicate().
+		 */
+		j = -1;
+		while ((j = bms_next_member(indexattrs, j)) >= 0)
+		{
+			AttrNumber	attno = j + FirstLowInvalidHeapAttributeNumber;
+
+			if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("index creation on virtual generated columns is not supported")));
+		}
 	}
 
 	/* Is index safe for others to ignore?  See set_indexsafe_procflags() */
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index 1db3ef69d22..744b3508c24 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -246,6 +246,12 @@ CreateStatistics(CreateStatsStmt *stmt)
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("statistics creation on system columns is not supported")));
 
+			/* Disallow use of virtual generated columns in extended stats */
+			if (attForm->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("statistics creation on virtual generated columns is not supported")));
+
 			/* Disallow data types without a less-than operator */
 			type = lookup_type_cache(attForm->atttypid, TYPECACHE_LT_OPR);
 			if (type->lt_opr == InvalidOid)
@@ -269,6 +275,12 @@ CreateStatistics(CreateStatsStmt *stmt)
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("statistics creation on system columns is not supported")));
 
+			/* Disallow use of virtual generated columns in extended stats */
+			if (get_attgenerated(relid, var->varattno) == ATTRIBUTE_GENERATED_VIRTUAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("statistics creation on virtual generated columns is not supported")));
+
 			/* Disallow data types without a less-than operator */
 			type = lookup_type_cache(var->vartype, TYPECACHE_LT_OPR);
 			if (type->lt_opr == InvalidOid)
@@ -290,7 +302,6 @@ CreateStatistics(CreateStatsStmt *stmt)
 
 			Assert(expr != NULL);
 
-			/* Disallow expressions referencing system attributes. */
 			pull_varattnos(expr, 1, &attnums);
 
 			k = -1;
@@ -298,10 +309,17 @@ CreateStatistics(CreateStatsStmt *stmt)
 			{
 				AttrNumber	attnum = k + FirstLowInvalidHeapAttributeNumber;
 
+				/* Disallow expressions referencing system attributes. */
 				if (attnum <= 0)
 					ereport(ERROR,
 							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 							 errmsg("statistics creation on system columns is not supported")));
+
+				/* Disallow use of virtual generated columns in extended stats */
+				if (get_attgenerated(relid, attnum) == ATTRIBUTE_GENERATED_VIRTUAL)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("statistics creation on virtual generated columns is not supported")));
 			}
 
 			/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 70cd2669164..2f83274daac 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -2936,6 +2936,15 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 									 errhint("A child table column cannot be generated unless its parent column is.")));
 					}
 
+					if (coldef->generated && restdef->generated && coldef->generated != restdef->generated)
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+								 errmsg("column \"%s\" inherits from generated column of different kind",
+										restdef->colname),
+								 errdetail("Parent column is %s, child column is %s.",
+										   coldef->generated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL",
+										   restdef->generated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL")));
+
 					/*
 					 * Override the parent's default value for this column
 					 * (coldef->cooked_default) with the partition's local
@@ -3207,6 +3216,15 @@ MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const
 					 errhint("A child table column cannot be generated unless its parent column is.")));
 	}
 
+	if (inhdef->generated && newdef->generated && newdef->generated != inhdef->generated)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+				 errmsg("column \"%s\" inherits from generated column of different kind",
+						inhdef->colname),
+				 errdetail("Parent column is %s, child column is %s.",
+						   inhdef->generated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL",
+						   newdef->generated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL")));
+
 	/*
 	 * If new def has a default, override previous default
 	 */
@@ -5986,7 +6004,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
 		{
 			case CONSTR_CHECK:
 				needscan = true;
-				con->qualstate = ExecPrepareExpr((Expr *) con->qual, estate);
+				con->qualstate = ExecPrepareExpr((Expr *) expand_generated_columns_in_expr(con->qual, newrel ? newrel : oldrel), estate);
 				break;
 			case CONSTR_FOREIGN:
 				/* Nothing to do here */
@@ -7704,6 +7722,14 @@ ATExecSetNotNull(AlteredTableInfo *tab, Relation rel,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
+	/* TODO: see transformColumnDefinition() */
+	if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("not-null constraints are not supported on virtual generated columns"),
+				 errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
+						   colName, RelationGetRelationName(rel))));
+
 	/*
 	 * Okay, actually perform the catalog change ... if needed
 	 */
@@ -8301,7 +8327,18 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
-	if (attTup->attgenerated != ATTRIBUTE_GENERATED_STORED)
+	/*
+	 * TODO: This could be done, but it would need a different implementation:
+	 * no rewriting, but still need to recheck any constraints.
+	 */
+	if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("ALTER TABLE / SET 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)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("column \"%s\" of relation \"%s\" is not a generated column",
@@ -8458,17 +8495,30 @@ ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMOD
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
-	if (attTup->attgenerated != ATTRIBUTE_GENERATED_STORED)
+	/*
+	 * TODO: This could be done, but it would need a table rewrite to
+	 * materialize the generated values.  Note that for the time being, we
+	 * still error with missing_ok, so that we don't silently leave the column
+	 * as generated.
+	 */
+	if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("ALTER TABLE / DROP EXPRESSION is not supported for virtual generated columns"),
+				 errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
+						   colName, RelationGetRelationName(rel))));
+
+	if (!attTup->attgenerated)
 	{
 		if (!missing_ok)
 			ereport(ERROR,
 					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-					 errmsg("column \"%s\" of relation \"%s\" is not a stored generated column",
+					 errmsg("column \"%s\" of relation \"%s\" is not a generated column",
 							colName, RelationGetRelationName(rel))));
 		else
 		{
 			ereport(NOTICE,
-					(errmsg("column \"%s\" of relation \"%s\" is not a stored generated column, skipping",
+					(errmsg("column \"%s\" of relation \"%s\" is not a generated column, skipping",
 							colName, RelationGetRelationName(rel))));
 			heap_freetuple(tuple);
 			table_close(attrelation, RowExclusiveLock);
@@ -8611,6 +8661,16 @@ ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
+	/*
+	 * Prevent this as long as the ANALYZE code skips virtual generated
+	 * columns.
+	 */
+	if (attrtuple->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot alter statistics on virtual generated column \"%s\"",
+						colName)));
+
 	if (rel->rd_rel->relkind == RELKIND_INDEX ||
 		rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX)
 	{
@@ -9690,6 +9750,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 						 errmsg("invalid %s action for foreign key constraint containing generated column",
 								"ON DELETE")));
 		}
+
+		/*
+		 * FKs on virtual columns are not supported.  This would require
+		 * various additional support in ri_triggers.c, including special
+		 * handling in ri_NullCheck(), ri_KeysEqual(),
+		 * RI_FKey_fk_upd_check_required() (since all virtual columns appear
+		 * as NULL there).  Also not really practical as long as you can't
+		 * index virtual columns.
+		 */
+		if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("foreign key constraints on virtual generated columns are not supported")));
 	}
 
 	/*
@@ -11671,7 +11744,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 			val = SysCacheGetAttrNotNull(CONSTROID, tuple,
 										 Anum_pg_constraint_conbin);
 			conbin = TextDatumGetCString(val);
-			newcon->qual = (Node *) stringToNode(conbin);
+			newcon->qual = (Node *) expand_generated_columns_in_expr(stringToNode(conbin), rel);
 
 			/* Find or create work queue entry for this table */
 			tab = ATGetQueueEntry(wqueue, rel);
@@ -12722,8 +12795,12 @@ ATPrepAlterColumnType(List **wqueue,
 					   list_make1_oid(rel->rd_rel->reltype),
 					   0);
 
-	if (tab->relkind == RELKIND_RELATION ||
-		tab->relkind == RELKIND_PARTITIONED_TABLE)
+	if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+	{
+		/* do nothing */
+	}
+	else if (tab->relkind == RELKIND_RELATION ||
+			 tab->relkind == RELKIND_PARTITIONED_TABLE)
 	{
 		/*
 		 * Set up an expression to transform the old data value to the new
@@ -12796,11 +12873,12 @@ ATPrepAlterColumnType(List **wqueue,
 				 errmsg("\"%s\" is not a table",
 						RelationGetRelationName(rel))));
 
-	if (!RELKIND_HAS_STORAGE(tab->relkind))
+	if (!RELKIND_HAS_STORAGE(tab->relkind) || attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
 	{
 		/*
-		 * For relations without storage, do this check now.  Regular tables
-		 * will check it later when the table is being rewritten.
+		 * For relations or columns without storage, do this check now.
+		 * Regular tables will check it later when the table is being
+		 * rewritten.
 		 */
 		find_composite_type_dependencies(rel->rd_rel->reltype, rel, NULL);
 	}
@@ -15758,6 +15836,14 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 						(errcode(ERRCODE_DATATYPE_MISMATCH),
 						 errmsg("column \"%s\" in child table must not be a generated column", parent_attname)));
 
+			if (parent_att->attgenerated && child_att->attgenerated && child_att->attgenerated != parent_att->attgenerated)
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("column \"%s\" inherits from generated column of different kind", parent_attname),
+						 errdetail("Parent column is %s, child column is %s.",
+								   parent_att->attgenerated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL",
+								   child_att->attgenerated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL")));
+
 			/*
 			 * Regular inheritance children are independent enough not to
 			 * inherit identity columns.  But partitions are integral part of
@@ -17884,8 +17970,11 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 						 parser_errposition(pstate, pelem->location)));
 
 			/*
-			 * Generated columns cannot work: They are computed after BEFORE
-			 * triggers, but partition routing is done before all triggers.
+			 * Stored generated columns cannot work: They are computed after
+			 * BEFORE triggers, but partition routing is done before all
+			 * triggers.  Maybe virtual generated columns could be made to
+			 * work, but then they would need to be handled as an expression
+			 * below.
 			 */
 			if (attform->attgenerated)
 				ereport(ERROR,
@@ -17967,9 +18056,12 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 				}
 
 				/*
-				 * Generated columns cannot work: They are computed after
-				 * BEFORE triggers, but partition routing is done before all
-				 * triggers.
+				 * Stored generated columns cannot work: They are computed
+				 * after BEFORE triggers, but partition routing is done before
+				 * all triggers.  Virtual generated columns could probably
+				 * work, but it would require more work elsewhere (for example
+				 * SET EXPRESSION would need to check whether the column is
+				 * used in partition keys).  Seems safer to prohibit for now.
 				 */
 				i = -1;
 				while ((i = bms_next_member(expr_attrs, i)) >= 0)
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 170360edda8..021328746a1 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -43,6 +43,7 @@
 #include "parser/parse_relation.h"
 #include "partitioning/partdesc.h"
 #include "pgstat.h"
+#include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "storage/lmgr.h"
 #include "utils/acl.h"
@@ -101,6 +102,7 @@ static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 								  bool is_crosspart_update);
 static void AfterTriggerEnlargeQueryState(void);
 static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
+static void check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple tuple);

 /*
@@ -641,7 +643,8 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 					if (TRIGGER_FOR_BEFORE(tgtype) &&
 						var->varattno == 0 &&
 						RelationGetDescr(rel)->constr &&
-						RelationGetDescr(rel)->constr->has_generated_stored)
+						(RelationGetDescr(rel)->constr->has_generated_stored ||
+						 RelationGetDescr(rel)->constr->has_generated_virtual))
 						ereport(ERROR,
 								(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 								 errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
@@ -942,6 +945,13 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 						 errmsg("column \"%s\" of relation \"%s\" does not exist",
 								name, RelationGetRelationName(rel))));
 
+			/* Currently doesn't work. */
+			if (TupleDescAttr(RelationGetDescr(rel), attnum - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				ereport(ERROR,
+						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						errmsg("virtual generated columns are not supported as trigger columns"),
+						errdetail("Column \"%s\" is a virtual generated column.", name));
+
 			/* Check for duplicates */
 			for (j = i - 1; j >= 0; j--)
 			{
@@ -2502,6 +2512,8 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
 		}
 		else if (newtuple != oldtuple)
 		{
+			check_modified_virtual_generated(RelationGetDescr(relinfo->ri_RelationDesc), newtuple);
+
 			ExecForceStoreHeapTuple(newtuple, slot, false);
 
 			/*
@@ -3059,6 +3071,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
 		}
 		else if (newtuple != oldtuple)
 		{
+			check_modified_virtual_generated(RelationGetDescr(relinfo->ri_RelationDesc), newtuple);
+
 			ExecForceStoreHeapTuple(newtuple, newslot, false);
 
 			/*
@@ -3489,6 +3503,7 @@ TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
 
 			oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
 			tgqual = stringToNode(trigger->tgqual);
+			tgqual = (Node *) expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc);
 			/* Change references to OLD and NEW to INNER_VAR and OUTER_VAR */
 			ChangeVarNodes(tgqual, PRS2_OLD_VARNO, INNER_VAR, 0);
 			ChangeVarNodes(tgqual, PRS2_NEW_VARNO, OUTER_VAR, 0);
@@ -6596,3 +6611,34 @@ pg_trigger_depth(PG_FUNCTION_ARGS)
 {
 	PG_RETURN_INT32(MyTriggerDepth);
 }
+
+/*
+ * Check whether a trigger modified a virtual generated column and error if
+ * so.
+ *
+ * We need to check this so that we don't end up storing a non-null value in a
+ * virtual generated column.
+ *
+ * We don't need to check for stored generated columns, since those will be
+ * overwritten later anyway.
+ */
+static void
+check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple tuple)
+{
+	if (!(tupdesc->constr && tupdesc->constr->has_generated_virtual))
+		return;
+
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+		{
+			bool		isnull;
+
+			fastgetattr(tuple, i + 1, tupdesc, &isnull);
+			if (!isnull)
+				ereport(ERROR,
+						(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+						 errmsg("trigger modified virtual generated column value")));
+		}
+	}
+}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 77394e76c37..58b0f2f76d6 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -2119,6 +2119,10 @@ CheckVarSlotCompatibility(TupleTableSlot *slot, int attnum, Oid vartype)
 
 		attr = TupleDescAttr(slot_tupdesc, attnum - 1);
 
+		/* Internal error: somebody forgot to expand it. */
+		if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			elog(ERROR, "unexpected virtual generated column reference");
+
 		if (attr->attisdropped)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 29e186fa73d..a49b1e821e0 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1743,6 +1743,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
 			Expr	   *checkconstr;
 
 			checkconstr = stringToNode(check[i].ccbin);
+			checkconstr = (Expr *) expand_generated_columns_in_expr((Node *) checkconstr, rel);
 			resultRelInfo->ri_ConstraintExprs[i] =
 				ExecPrepareExpr(checkconstr, estate);
 		}
@@ -2290,7 +2291,9 @@ ExecBuildSlotValueDescription(Oid reloid,
 
 		if (table_perm || column_perm)
 		{
-			if (slot->tts_isnull[i])
+			if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				val = "virtual";
+			else if (slot->tts_isnull[i])
 				val = "null";
 			else
 			{
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index e901203424d..e3936a0b8ea 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -568,6 +568,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;
 
 	assign_query_collations(pstate, qry);
 
@@ -994,6 +995,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;
 
 	assign_query_collations(pstate, qry);
 
@@ -1459,6 +1461,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;
 
 	foreach(l, stmt->lockingClause)
 	{
@@ -1685,6 +1688,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	qry->jointree = makeFromExpr(pstate->p_joinlist, NULL);
 
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;
 
 	assign_query_collations(pstate, qry);
 
@@ -1936,6 +1940,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;
 
 	foreach(l, lockingClause)
 	{
@@ -2410,6 +2415,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;
 
 	assign_query_collations(pstate, qry);
 
@@ -2477,6 +2483,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;
 
 	assign_query_collations(pstate, qry);
 
@@ -2843,6 +2850,7 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
+	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;
 
 	foreach(l, sstmt->lockingClause)
 	{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 84cef57a707..18ce875c406 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -640,7 +640,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <str>		opt_existing_window_name
 %type <boolean> opt_if_not_exists
 %type <boolean> opt_unique_null_treatment
-%type <ival>	generated_when override_kind
+%type <ival>	generated_when override_kind opt_virtual_or_stored
 %type <partspec>	PartitionSpec OptPartitionSpec
 %type <partelem>	part_elem
 %type <list>		part_params
@@ -788,7 +788,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	UNLISTEN UNLOGGED UNTIL UPDATE USER USING
 
 	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
-	VERBOSE VERSION_P VIEW VIEWS VOLATILE
+	VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE
 
 	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
@@ -3973,7 +3973,7 @@ ColConstraintElem:
 					n->location = @1;
 					$$ = (Node *) n;
 				}
-			| GENERATED generated_when AS '(' a_expr ')' STORED
+			| GENERATED generated_when AS '(' a_expr ')' opt_virtual_or_stored
 				{
 					Constraint *n = makeNode(Constraint);
 
@@ -3981,6 +3981,7 @@ ColConstraintElem:
 					n->generated_when = $2;
 					n->raw_expr = $5;
 					n->cooked_expr = NULL;
+					n->generated_kind = $7;
 					n->location = @1;
 
 					/*
@@ -4027,6 +4028,12 @@ generated_when:
 			| BY DEFAULT	{ $$ = ATTRIBUTE_IDENTITY_BY_DEFAULT; }
 		;
 
+opt_virtual_or_stored:
+			STORED			{ $$ = ATTRIBUTE_GENERATED_STORED; }
+			| VIRTUAL		{ $$ = ATTRIBUTE_GENERATED_VIRTUAL; }
+			| /*EMPTY*/		{ $$ = ATTRIBUTE_GENERATED_VIRTUAL; }
+		;
+
 /*
  * ConstraintAttr represents constraint attributes, which we parse as if
  * they were independent constraint clauses, in order to avoid shift/reduce
@@ -17809,6 +17816,7 @@ unreserved_keyword:
 			| VERSION_P
 			| VIEW
 			| VIEWS
+			| VIRTUAL
 			| VOLATILE
 			| WHITESPACE_P
 			| WITHIN
@@ -18462,6 +18470,7 @@ bare_label_keyword:
 			| VERSION_P
 			| VIEW
 			| VIEWS
+			| VIRTUAL
 			| VOLATILE
 			| WHEN
 			| WHITESPACE_P
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 8118036495b..48524ac3fc2 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -207,6 +207,10 @@ setTargetTable(ParseState *pstate, RangeVar *relation,
 	pstate->p_target_relation = parserOpenTable(pstate, relation,
 												RowExclusiveLock);
 
+	if (pstate->p_target_relation->rd_att->constr &&
+		pstate->p_target_relation->rd_att->constr->has_generated_virtual)
+		pstate->p_hasGeneratedVirtual = true;
+
 	/*
 	 * Now build an RTE and a ParseNamespaceItem.
 	 */
diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c
index 87df79027d7..350e9e18885 100644
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
 
 	qry->hasTargetSRFs = false;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
+	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;
 
 	assign_query_collations(pstate, qry);
 
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 2f64eaf0e37..b864749f09c 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -704,7 +704,11 @@ scanNSItemForColumn(ParseState *pstate, ParseNamespaceItem *nsitem,
 						colname),
 				 parser_errposition(pstate, location)));
 
-	/* In generated column, no system column is allowed except tableOid */
+	/*
+	 * In generated column, no system column is allowed except tableOid.
+	 * (Required for stored generated, but we also do it for virtual generated
+	 * for now for consistency.)
+	 */
 	if (pstate->p_expr_kind == EXPR_KIND_GENERATED_COLUMN &&
 		attnum < InvalidAttrNumber && attnum != TableOidAttributeNumber)
 		ereport(ERROR,
@@ -1511,6 +1515,9 @@ addRangeTableEntry(ParseState *pstate,
 	rte->eref = makeAlias(refname, NIL);
 	buildRelationAliases(rel->rd_att, alias, rte->eref);
 
+	if (rel->rd_att->constr && rel->rd_att->constr->has_generated_virtual)
+		pstate->p_hasGeneratedVirtual = true;
+
 	/*
 	 * Set flags and initialize access permissions.
 	 *
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 79cad4ab30c..03301f9c53e 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -751,7 +751,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 									column->colname, cxt->relation->relname),
 							 parser_errposition(cxt->pstate,
 												constraint->location)));
-				column->generated = ATTRIBUTE_GENERATED_STORED;
+				column->generated = constraint->generated_kind;
 				column->raw_default = constraint->raw_expr;
 				Assert(constraint->cooked_expr == NULL);
 				saw_generated = true;
@@ -839,6 +839,18 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 							column->colname, cxt->relation->relname),
 					 parser_errposition(cxt->pstate,
 										constraint->location)));
+
+		/*
+		 * TODO: Straightforward not-null constraints won't work on virtual
+		 * generated columns, because there is no support for expanding the
+		 * column when the constraint is checked.  Maybe we could convert the
+		 * not-null constraint into a full check constraint, so that the
+		 * generation expression can be expanded at check time.
+		 */
+		if (column->is_not_null && column->generated == ATTRIBUTE_GENERATED_VIRTUAL)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("not-null constraints are not supported on virtual generated columns")));
 	}
 
 	/*
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index abef4eaf68b..52dc9b5fcac 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -27,6 +27,7 @@
 #include "replication/logicalproto.h"
 #include "replication/origin.h"
 #include "replication/pgoutput.h"
+#include "rewrite/rewriteHandler.h"
 #include "utils/builtins.h"
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
@@ -996,7 +997,7 @@ pgoutput_row_filter_init(PGOutputData *data, List *publications,
 				continue;
 
 			foreach(lc, rfnodes[idx])
-				filters = lappend(filters, stringToNode((char *) lfirst(lc)));
+				filters = lappend(filters, expand_generated_columns_in_expr(stringToNode((char *) lfirst(lc)), relation));
 
 			/* combine the row filter and cache the ExprState */
 			rfnode = make_orclause(filters);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index c223a2c50af..03b4b857ed1 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -44,6 +44,7 @@
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"

 /* We use a list of these to detect recursion in RewriteQuery */
@@ -90,6 +91,8 @@ static List *matchLocks(CmdType event, Relation relation,
 						int varno, Query *parsetree, bool *hasUpdate);
 static Query *fireRIRrules(Query *parsetree, List *activeRIRs);
 static Bitmapset *adjust_view_column_set(Bitmapset *cols, List *targetlist);
+struct expand_generated_context;
+static Query *expand_generated_columns_in_query(Query *query, struct expand_generated_context *context);

 /*
@@ -974,7 +977,8 @@ rewriteTargetListIU(List *targetList,
 		if (att_tup->attgenerated)
 		{
 			/*
-			 * stored generated column will be fixed in executor
+			 * virtual generated column stores a null value; stored generated
+			 * column will be fixed in executor
 			 */
 			new_tle = NULL;
 		}
@@ -4365,6 +4369,168 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
 }

+/*
+ * Virtual generated columns support
+ */
+
+struct expand_generated_context
+{
+	/* list of range tables, innermost last */
+	List	   *rtables;
+
+	/* incremented for every level where it's true */
+	int			ancestor_has_virtual;
+};
+
+static Node *
+expand_generated_columns_mutator(Node *node, struct expand_generated_context *context)
+{
+	if (node == NULL)
+		return NULL;
+
+	if (IsA(node, Var))
+	{
+		Var		   *v = (Var *) node;
+		Oid			relid;
+		AttrNumber	attnum;
+		List	   *rtable = list_nth_node(List,
+										   context->rtables,
+										   list_length(context->rtables) - v->varlevelsup - 1);
+
+		relid = rt_fetch(v->varno, rtable)->relid;
+		attnum = v->varattno;
+
+		if (!relid || !attnum)
+			return node;
+
+		if (get_attgenerated(relid, attnum) == ATTRIBUTE_GENERATED_VIRTUAL)
+		{
+			Relation	rt_entry_relation = table_open(relid, NoLock);
+			Oid			attcollid;
+
+			node = build_column_default(rt_entry_relation, attnum);
+			if (node == NULL)
+				elog(ERROR, "no generation expression found for column number %d of table \"%s\"",
+					 attnum, RelationGetRelationName(rt_entry_relation));
+
+			/*
+			 * If the column definition has a collation and it is different
+			 * from the collation of the generation expression, put a COLLATE
+			 * clause around the expression.
+			 */
+			attcollid = GetSysCacheOid(ATTNUM, Anum_pg_attribute_attcollation, relid, attnum, 0, 0);
+			if (attcollid && attcollid != exprCollation(node))
+			{
+				CollateExpr *ce = makeNode(CollateExpr);
+
+				ce->arg = (Expr *) node;
+				ce->collOid = attcollid;
+				ce->location = -1;
+
+				node = (Node *) ce;
+			}
+
+			IncrementVarSublevelsUp(node, v->varlevelsup, 0);
+			ChangeVarNodes(node, 1, v->varno, v->varlevelsup);
+
+			table_close(rt_entry_relation, NoLock);
+		}
+
+		return node;
+	}
+	else if (IsA(node, Query))
+	{
+		Query	   *query = (Query *) node;
+
+		query = expand_generated_columns_in_query(query, context);
+
+		return (Node *) query;
+	}
+	else
+		return expression_tree_mutator(node, expand_generated_columns_mutator, context);
+}
+
+Node *
+expand_generated_columns_in_expr(Node *node, Relation rel)
+{
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+
+	if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
+	{
+		RangeTblEntry *rte;
+		List	   *rtable;
+		struct expand_generated_context context;
+
+		/*
+		 * Make a dummy range table for a single relation.  For the benefit of
+		 * triggers, add the same entry twice, so it covers PRS2_OLD_VARNO and
+		 * PRS2_NEW_VARNO.
+		 */
+		rte = makeNode(RangeTblEntry);
+		rte->relid = RelationGetRelid(rel);
+		rtable = list_make2(rte, rte);
+		context.rtables = list_make1(rtable);
+
+		return expression_tree_mutator(node, expand_generated_columns_mutator, &context);
+	}
+	else
+		return node;
+}
+
+/*
+ * Expand virtual generated columns in a Query.  We do some optimizations here
+ * to avoid digging through the whole Query unless necessary.
+ */
+static Query *
+expand_generated_columns_in_query(Query *query, struct expand_generated_context *context)
+{
+	context->rtables = lappend(context->rtables, query->rtable);
+	if (query->hasGeneratedVirtual)
+		context->ancestor_has_virtual++;
+
+	/*
+	 * If any table in the query has a virtual column or there is a sublink,
+	 * then we need to do the whole walk.
+	 */
+	if (query->hasGeneratedVirtual || query->hasSubLinks || context->ancestor_has_virtual)
+	{
+		query = query_tree_mutator(query,
+								   expand_generated_columns_mutator,
+								   context,
+								   QTW_DONT_COPY_QUERY);
+	}
+
+	/*
+	 * Else we only need to process subqueries.
+	 */
+	else
+	{
+		ListCell   *lc;
+
+		foreach(lc, query->rtable)
+		{
+			RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
+
+			if (rte->rtekind == RTE_SUBQUERY)
+				rte->subquery = expand_generated_columns_in_query(rte->subquery, context);
+		}
+
+		foreach(lc, query->cteList)
+		{
+			CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
+
+			cte->ctequery = (Node *) expand_generated_columns_in_query(castNode(Query, cte->ctequery), context);
+		}
+	}
+
+	if (query->hasGeneratedVirtual)
+		context->ancestor_has_virtual--;
+	context->rtables = list_truncate(context->rtables, list_length(context->rtables) - 1);
+
+	return query;
+}
+
+
 /*
  * QueryRewrite -
  *	  Primary entry point to the query rewriter.
@@ -4409,9 +4575,12 @@ QueryRewrite(Query *parsetree)
 	foreach(l, querylist)
 	{
 		Query	   *query = (Query *) lfirst(l);
+		struct expand_generated_context context = {0};
 
 		query = fireRIRrules(query, NIL);
 
+		query = expand_generated_columns_in_query(query, &context);
+
 		query->queryId = input_query_id;
 
 		results = lappend(results, query);
diff --git a/src/backend/utils/cache/partcache.c b/src/backend/utils/cache/partcache.c
index beec6cddbc4..1dee7c1e899 100644
--- a/src/backend/utils/cache/partcache.c
+++ b/src/backend/utils/cache/partcache.c
@@ -26,6 +26,7 @@
 #include "nodes/nodeFuncs.h"
 #include "optimizer/optimizer.h"
 #include "partitioning/partbounds.h"
+#include "rewrite/rewriteHandler.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -148,6 +149,8 @@ RelationBuildPartitionKey(Relation relation)
 		expr = stringToNode(exprString);
 		pfree(exprString);
 
+		expr = expand_generated_columns_in_expr(expr, relation);
+
 		/*
 		 * Run the expressions through const-simplification since the planner
 		 * will be comparing them to similarly-processed qual clause operands,
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 66ed24e4012..4f756acd39d 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -589,6 +589,8 @@ RelationBuildTupleDesc(Relation relation)
 			constr->has_not_null = true;
 		if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
 			constr->has_generated_stored = true;
+		if (attp->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			constr->has_generated_virtual = true;
 		if (attp->atthasdef)
 			ndef++;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b6e01d3d292..4b2aac8cd33 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -15998,6 +15998,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
 							appendPQExpBuffer(q, " GENERATED ALWAYS AS (%s) STORED",
 											  tbinfo->attrdefs[j]->adef_expr);
+						else if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_VIRTUAL)
+							appendPQExpBuffer(q, " GENERATED ALWAYS AS (%s)",
+											  tbinfo->attrdefs[j]->adef_expr);
 						else
 							appendPQExpBuffer(q, " DEFAULT %s",
 											  tbinfo->attrdefs[j]->adef_expr);
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 5bcc2244d58..c3d6022015c 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -3595,12 +3595,14 @@
 		create_order => 3,
 		create_sql => 'CREATE TABLE dump_test.test_table_generated (
 						   col1 int primary key,
-						   col2 int generated always as (col1 * 2) stored
+						   col2 int generated always as (col1 * 2) stored,
+						   col3 int generated always as (col1 * 3) virtual
 					   );',
 		regexp => qr/^
 			\QCREATE TABLE dump_test.test_table_generated (\E\n
 			\s+\Qcol1 integer NOT NULL,\E\n
-			\s+\Qcol2 integer GENERATED ALWAYS AS ((col1 * 2)) STORED\E\n
+			\s+\Qcol2 integer GENERATED ALWAYS AS ((col1 * 2)) STORED,\E\n
+			\s+\Qcol3 integer GENERATED ALWAYS AS ((col1 * 3))\E\n
 			\);
 			/xms,
 		like =>
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 7c9a1f234c6..4309d3e757d 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2089,6 +2089,12 @@ describeOneTableDetails(const char *schemaname,
 									   PQgetvalue(res, i, attrdef_col));
 				mustfree = true;
 			}
+			else if (generated[0] == ATTRIBUTE_GENERATED_VIRTUAL)
+			{
+				default_str = psprintf("generated always as (%s)",
+									   PQgetvalue(res, i, attrdef_col));
+				mustfree = true;
+			}
 			else
 				default_str = PQgetvalue(res, i, attrdef_col);
 
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 8930a28d660..6ef40249583 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -43,6 +43,7 @@ typedef struct TupleConstr
 	uint16		num_check;
 	bool		has_not_null;
 	bool		has_generated_stored;
+	bool		has_generated_virtual;
 } TupleConstr;
 
 /*
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index c512824cd1c..235ad73eedc 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_IS_VIRTUAL		0x08	/* is virtual generated column */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 1c62b8bfcb5..8f158dc6083 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -234,6 +234,7 @@ MAKE_SYSCACHE(ATTNUM, pg_attribute_relid_attnum_index, 128);
 #define		  ATTRIBUTE_IDENTITY_BY_DEFAULT 'd'
 
 #define		  ATTRIBUTE_GENERATED_STORED	's'
+#define		  ATTRIBUTE_GENERATED_VIRTUAL	'v'
 
 #endif							/* EXPOSE_TO_CLIENT_CODE */
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 124d853e499..355f52199fa 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -160,6 +160,8 @@ typedef struct Query
 	bool		hasForUpdate pg_node_attr(query_jumble_ignore);
 	/* rewriter has applied some RLS policy */
 	bool		hasRowSecurity pg_node_attr(query_jumble_ignore);
+	/* some table has a virtual generated column */
+	bool		hasGeneratedVirtual pg_node_attr(query_jumble_ignore);
 	/* is a RETURN statement */
 	bool		isReturn pg_node_attr(query_jumble_ignore);
 
@@ -2730,6 +2732,7 @@ typedef struct Constraint
 	char	   *cooked_expr;	/* CHECK or DEFAULT expression, as
 								 * nodeToString representation */
 	char		generated_when; /* ALWAYS or BY DEFAULT */
+	char		generated_kind; /* STORED or VIRTUAL */
 	int			inhcount;		/* initial inheritance count to apply, for
 								 * "raw" NOT NULL constraints */
 	bool		nulls_not_distinct; /* null treatment for UNIQUE constraints */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f8659078ced..6416f9b110b 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -489,6 +489,7 @@ PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("version", VERSION_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("view", VIEW, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("views", VIEWS, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("virtual", VIRTUAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("volatile", VOLATILE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("when", WHEN, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("where", WHERE, RESERVED_KEYWORD, AS_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 5b781d87a9d..1b02128548b 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -225,6 +225,7 @@ struct ParseState
 	bool		p_hasTargetSRFs;
 	bool		p_hasSubLinks;
 	bool		p_hasModifyingCTE;
+	bool		p_hasGeneratedVirtual;
 
 	Node	   *p_last_srf;		/* most recent set-returning func/op found */
 
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index 1b65cda71cf..a646a20675a 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -38,4 +38,6 @@ extern void error_view_not_updatable(Relation view,
 									 List *mergeActionList,
 									 const char *detail);
 
+extern Node *expand_generated_columns_in_expr(Node *node, Relation rel);
+
 #endif							/* REWRITEHANDLER_H */
diff --git a/src/pl/plperl/expected/plperl_trigger.out b/src/pl/plperl/expected/plperl_trigger.out
index d4879e2f03b..42c52ecbba8 100644
--- a/src/pl/plperl/expected/plperl_trigger.out
+++ b/src/pl/plperl/expected/plperl_trigger.out
@@ -8,7 +8,8 @@ CREATE TABLE trigger_test (
 );
 CREATE TABLE trigger_test_generated (
     i int,
-    j int GENERATED ALWAYS AS (i * 2) STORED
+    j int GENERATED ALWAYS AS (i * 2) STORED,
+    k int GENERATED ALWAYS AS (i * 3) VIRTUAL
 );
 CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger LANGUAGE plperl AS $$
 
@@ -386,7 +387,7 @@ INSERT INTO trigger_test_generated (i) VALUES (1);
 ERROR:  cannot set generated column "j"
 CONTEXT:  PL/Perl function "generated_test_func1"
 SELECT * FROM trigger_test_generated;
- i | j 
----+---
+ i | j | k 
+---+---+---
 (0 rows)
 
diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c
index d68ad7be345..9611aa82352 100644
--- a/src/pl/plperl/plperl.c
+++ b/src/pl/plperl/plperl.c
@@ -3052,6 +3052,9 @@ plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc, bool include_generate
 			/* don't include unless requested */
 			if (!include_generated)
 				continue;
+			/* never include virtual columns */
+			if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				continue;
 		}
 
 		attname = NameStr(att->attname);
diff --git a/src/pl/plperl/sql/plperl_trigger.sql b/src/pl/plperl/sql/plperl_trigger.sql
index 4adddeb80ac..2798a02fa12 100644
--- a/src/pl/plperl/sql/plperl_trigger.sql
+++ b/src/pl/plperl/sql/plperl_trigger.sql
@@ -10,7 +10,8 @@ CREATE TABLE trigger_test (
 
 CREATE TABLE trigger_test_generated (
     i int,
-    j int GENERATED ALWAYS AS (i * 2) STORED
+    j int GENERATED ALWAYS AS (i * 2) STORED,
+    k int GENERATED ALWAYS AS (i * 3) VIRTUAL
 );
 
 CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger LANGUAGE plperl AS $$
diff --git a/src/pl/plpython/expected/plpython_trigger.out b/src/pl/plpython/expected/plpython_trigger.out
index 4cb90cb5204..64eab2fa3f4 100644
--- a/src/pl/plpython/expected/plpython_trigger.out
+++ b/src/pl/plpython/expected/plpython_trigger.out
@@ -69,7 +69,8 @@ CREATE TABLE trigger_test
 	(i int, v text );
 CREATE TABLE trigger_test_generated (
 	i int,
-        j int GENERATED ALWAYS AS (i * 2) STORED
+        j int GENERATED ALWAYS AS (i * 2) STORED,
+        k int GENERATED ALWAYS AS (i * 3) VIRTUAL
 );
 CREATE FUNCTION trigger_data() RETURNS trigger LANGUAGE plpython3u AS $$
 
@@ -614,8 +615,8 @@ ERROR:  cannot set generated column "j"
 CONTEXT:  while modifying trigger row
 PL/Python function "generated_test_func1"
 SELECT * FROM trigger_test_generated;
- i | j 
----+---
+ i | j | k 
+---+---+---
 (0 rows)
 
 -- recursive call of a trigger mustn't corrupt TD (bug #18456)
diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c
index db14c5f8dae..51e1d610259 100644
--- a/src/pl/plpython/plpy_typeio.c
+++ b/src/pl/plpython/plpy_typeio.c
@@ -844,6 +844,9 @@ PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool inclu
 				/* don't include unless requested */
 				if (!include_generated)
 					continue;
+				/* never include virtual columns */
+				if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+					continue;
 			}
 
 			key = NameStr(attr->attname);
diff --git a/src/pl/plpython/sql/plpython_trigger.sql b/src/pl/plpython/sql/plpython_trigger.sql
index f6c2ef8d6a0..440549c0785 100644
--- a/src/pl/plpython/sql/plpython_trigger.sql
+++ b/src/pl/plpython/sql/plpython_trigger.sql
@@ -69,7 +69,8 @@ CREATE TABLE trigger_test
 
 CREATE TABLE trigger_test_generated (
 	i int,
-        j int GENERATED ALWAYS AS (i * 2) STORED
+        j int GENERATED ALWAYS AS (i * 2) STORED,
+        k int GENERATED ALWAYS AS (i * 3) VIRTUAL
 );
 
 CREATE FUNCTION trigger_data() RETURNS trigger LANGUAGE plpython3u AS $$
diff --git a/src/pl/tcl/expected/pltcl_trigger.out b/src/pl/tcl/expected/pltcl_trigger.out
index 129abd5ba67..5298e50a5ec 100644
--- a/src/pl/tcl/expected/pltcl_trigger.out
+++ b/src/pl/tcl/expected/pltcl_trigger.out
@@ -63,7 +63,8 @@ CREATE TABLE trigger_test (
 ALTER TABLE trigger_test DROP dropme;
 CREATE TABLE trigger_test_generated (
    i int,
-   j int GENERATED ALWAYS AS (i * 2) STORED
+   j int GENERATED ALWAYS AS (i * 2) STORED,
+   k int GENERATED ALWAYS AS (i * 3) VIRTUAL
 );
 CREATE VIEW trigger_test_view AS SELECT i, v FROM trigger_test;
 CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
@@ -647,7 +648,7 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig_before
 NOTICE:  TG_op: INSERT
-NOTICE:  TG_relatts: {{} i j}
+NOTICE:  TG_relatts: {{} i j k}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_generated
 NOTICE:  TG_table_schema: public
@@ -658,7 +659,7 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig_after
 NOTICE:  TG_op: INSERT
-NOTICE:  TG_relatts: {{} i j}
+NOTICE:  TG_relatts: {{} i j k}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_generated
 NOTICE:  TG_table_schema: public
@@ -670,7 +671,7 @@ NOTICE:  OLD: {i: 1, j: 2}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig_before
 NOTICE:  TG_op: UPDATE
-NOTICE:  TG_relatts: {{} i j}
+NOTICE:  TG_relatts: {{} i j k}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_generated
 NOTICE:  TG_table_schema: public
@@ -681,7 +682,7 @@ NOTICE:  OLD: {i: 1, j: 2}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig_after
 NOTICE:  TG_op: UPDATE
-NOTICE:  TG_relatts: {{} i j}
+NOTICE:  TG_relatts: {{} i j k}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_generated
 NOTICE:  TG_table_schema: public
@@ -693,7 +694,7 @@ NOTICE:  OLD: {i: 11, j: 22}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig_before
 NOTICE:  TG_op: DELETE
-NOTICE:  TG_relatts: {{} i j}
+NOTICE:  TG_relatts: {{} i j k}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_generated
 NOTICE:  TG_table_schema: public
@@ -704,7 +705,7 @@ NOTICE:  OLD: {i: 11, j: 22}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig_after
 NOTICE:  TG_op: DELETE
-NOTICE:  TG_relatts: {{} i j}
+NOTICE:  TG_relatts: {{} i j k}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_generated
 NOTICE:  TG_table_schema: public
@@ -882,7 +883,7 @@ TRUNCATE trigger_test_generated;
 INSERT INTO trigger_test_generated (i) VALUES (1);
 ERROR:  cannot set generated column "j"
 SELECT * FROM trigger_test_generated;
- i | j 
----+---
+ i | j | k 
+---+---+---
 (0 rows)
 
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index 21b2b045933..cb715ad08b6 100644
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -3206,6 +3206,9 @@ pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc, bool include_gene
 			/* don't include unless requested */
 			if (!include_generated)
 				continue;
+			/* never include virtual columns */
+			if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				continue;
 		}
 
 		/************************************************************
diff --git a/src/pl/tcl/sql/pltcl_trigger.sql b/src/pl/tcl/sql/pltcl_trigger.sql
index 2a244de83bc..0ed00f49526 100644
--- a/src/pl/tcl/sql/pltcl_trigger.sql
+++ b/src/pl/tcl/sql/pltcl_trigger.sql
@@ -73,7 +73,8 @@ CREATE TABLE trigger_test (
 
 CREATE TABLE trigger_test_generated (
    i int,
-   j int GENERATED ALWAYS AS (i * 2) STORED
+   j int GENERATED ALWAYS AS (i * 2) STORED,
+   k int GENERATED ALWAYS AS (i * 3) VIRTUAL
 );
 
 CREATE VIEW trigger_test_view AS SELECT i, v FROM trigger_test;
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index 6bfc6d040ff..97157dc635b 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -113,19 +113,20 @@ SELECT * FROM test_like_id_3;  -- identity was copied and applied
 (1 row)
 
 DROP TABLE test_like_id_1, test_like_id_2, test_like_id_3;
-CREATE TABLE test_like_gen_1 (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE test_like_gen_1 (a int, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (a * 3) VIRTUAL);
 \d test_like_gen_1
                         Table "public.test_like_gen_1"
  Column |  Type   | Collation | Nullable |              Default               
 --------+---------+-----------+----------+------------------------------------
  a      | integer |           |          | 
  b      | integer |           |          | generated always as (a * 2) stored
+ c      | integer |           |          | generated always as (a * 3)
 
 INSERT INTO test_like_gen_1 (a) VALUES (1);
 SELECT * FROM test_like_gen_1;
- a | b 
----+---
- 1 | 2
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
 (1 row)
 
 CREATE TABLE test_like_gen_2 (LIKE test_like_gen_1);
@@ -135,12 +136,13 @@ CREATE TABLE test_like_gen_2 (LIKE test_like_gen_1);
 --------+---------+-----------+----------+---------
  a      | integer |           |          | 
  b      | integer |           |          | 
+ c      | integer |           |          | 
 
 INSERT INTO test_like_gen_2 (a) VALUES (1);
 SELECT * FROM test_like_gen_2;
- a | b 
----+---
- 1 |  
+ a | b | c 
+---+---+---
+ 1 |   |  
 (1 row)
 
 CREATE TABLE test_like_gen_3 (LIKE test_like_gen_1 INCLUDING GENERATED);
@@ -150,12 +152,13 @@ CREATE TABLE test_like_gen_3 (LIKE test_like_gen_1 INCLUDING GENERATED);
 --------+---------+-----------+----------+------------------------------------
  a      | integer |           |          | 
  b      | integer |           |          | generated always as (a * 2) stored
+ c      | integer |           |          | generated always as (a * 3)
 
 INSERT INTO test_like_gen_3 (a) VALUES (1);
 SELECT * FROM test_like_gen_3;
- a | b 
----+---
- 1 | 2
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
 (1 row)
 
 DROP TABLE test_like_gen_1, test_like_gen_2, test_like_gen_3;
diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out
index 8ea8a3a92d2..8f0951e26a7 100644
--- a/src/test/regress/expected/generated_stored.out
+++ b/src/test/regress/expected/generated_stored.out
@@ -1,5 +1,5 @@
 -- sanity check of system catalog
-SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v');
  attrelid | attname | attgenerated 
 ----------+---------+--------------
 (0 rows)
@@ -217,6 +217,27 @@ SELECT * FROM gtestm ORDER BY id;
   2 | 20 | 200 | 40 | 400
 (2 rows)
 
+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
+  b int GENERATED ALWAYS AS (a * 2) STORED
+);
+INSERT INTO gtestm (a) SELECT g FROM generate_series(1, 10) g;
+MERGE INTO gtestm t USING gtestm AS s ON 2 * t.a = s.b WHEN MATCHED THEN DELETE RETURNING *;
+ a  | b  | a  | b  
+----+----+----+----
+  1 |  2 |  1 |  2
+  2 |  4 |  2 |  4
+  3 |  6 |  3 |  6
+  4 |  8 |  4 |  8
+  5 | 10 |  5 | 10
+  6 | 12 |  6 | 12
+  7 | 14 |  7 | 14
+  8 | 16 |  8 | 16
+  9 | 18 |  9 | 18
+ 10 | 20 | 10 | 20
+(10 rows)
+
 DROP TABLE gtestm;
 -- views
 CREATE VIEW gtest1v AS SELECT * FROM gtest1;
@@ -762,6 +783,11 @@ CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a *
 INSERT INTO gtest24 (a) VALUES (4);  -- ok
 INSERT INTO gtest24 (a) VALUES (6);  -- error
 ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+CREATE TYPE gtestdomain1range AS range (subtype = gtestdomain1);
+CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) STORED);
+INSERT INTO gtest24r (a) VALUES (4);  -- ok
+INSERT INTO gtest24r (a) VALUES (6);  -- error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
 -- 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);
@@ -1092,9 +1118,9 @@ SELECT * FROM gtest29;
 ALTER TABLE gtest29 ALTER COLUMN a SET EXPRESSION AS (a * 3);  -- error
 ERROR:  column "a" of relation "gtest29" is not a generated column
 ALTER TABLE gtest29 ALTER COLUMN a DROP EXPRESSION;  -- error
-ERROR:  column "a" of relation "gtest29" is not a stored generated column
+ERROR:  column "a" of relation "gtest29" is not a 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
+NOTICE:  column "a" of relation "gtest29" is not a generated column, skipping
 -- Change the expression
 ALTER TABLE gtest29 ALTER COLUMN b SET EXPRESSION AS (a * 3);
 SELECT * FROM gtest29;
diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_virtual.out
similarity index 67%
copy from src/test/regress/expected/generated_stored.out
copy to src/test/regress/expected/generated_virtual.out
index 8ea8a3a92d2..4cf4f8118f3 100644
--- a/src/test/regress/expected/generated_stored.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1,15 +1,15 @@
 -- sanity check of system catalog
-SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v');
  attrelid | attname | attgenerated 
 ----------+---------+--------------
 (0 rows)
 
-CREATE SCHEMA generated_stored_tests;
-GRANT USAGE ON SCHEMA generated_stored_tests TO PUBLIC;
-SET search_path = generated_stored_tests;
-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_schema = 'generated_stored_tests' ORDER BY 1, 2;
+CREATE SCHEMA generated_virtual_tests;
+GRANT USAGE ON SCHEMA generated_virtual_tests TO PUBLIC;
+SET search_path = generated_virtual_tests;
+CREATE TABLE gtest0 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (55) VIRTUAL);
+CREATE TABLE gtest1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
+SELECT table_name, column_name, column_default, is_nullable, is_generated, generation_expression FROM information_schema.columns WHERE table_schema = 'generated_virtual_tests' ORDER BY 1, 2;
  table_name | column_name | column_default | is_nullable | is_generated | generation_expression 
 ------------+-------------+----------------+-------------+--------------+-----------------------
  gtest0     | a           |                | NO          | NEVER        | 
@@ -18,89 +18,89 @@ SELECT table_name, column_name, column_default, is_nullable, is_generated, gener
  gtest1     | b           |                | YES         | ALWAYS       | (a * 2)
 (4 rows)
 
-SELECT table_name, column_name, dependent_column FROM information_schema.column_column_usage WHERE table_schema = 'generated_stored_tests' ORDER BY 1, 2, 3;
+SELECT table_name, column_name, dependent_column FROM information_schema.column_column_usage WHERE table_schema = 'generated_virtual_tests' ORDER BY 1, 2, 3;
  table_name | column_name | dependent_column 
 ------------+-------------+------------------
  gtest1     | a           | b
 (1 row)
 
 \d gtest1
-                    Table "generated_stored_tests.gtest1"
- Column |  Type   | Collation | Nullable |              Default               
---------+---------+-----------+----------+------------------------------------
+                Table "generated_virtual_tests.gtest1"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
  a      | integer |           | not null | 
- b      | integer |           |          | generated always as (a * 2) stored
+ b      | integer |           |          | generated always as (a * 2)
 Indexes:
     "gtest1_pkey" PRIMARY KEY, btree (a)
 
 -- 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);
+CREATE TABLE gtest_err_1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL GENERATED ALWAYS AS (a * 3) VIRTUAL);
 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 ...
+LINE 1: ...RY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL 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);
+CREATE TABLE gtest_err_2a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (b * 2) VIRTUAL);
 ERROR:  cannot use generated column "b" in column generation expression
-LINE 1: ...2a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (b * 2) STO...
+LINE 1: ...2a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (b * 2) VIR...
                                                              ^
 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);
+CREATE TABLE gtest_err_2b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL, c int GENERATED ALWAYS AS (b * 3) VIRTUAL);
 ERROR:  cannot use generated column "b" in column generation expression
-LINE 1: ...AYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (b * 3) STO...
+LINE 1: ...YS AS (a * 2) VIRTUAL, c int GENERATED ALWAYS AS (b * 3) VIR...
                                                              ^
 DETAIL:  A generated column cannot reference another generated column.
 -- a whole-row var is a self-reference on steroids, so disallow that too
 CREATE TABLE gtest_err_2c (a int PRIMARY KEY,
-    b int GENERATED ALWAYS AS (num_nulls(gtest_err_2c)) STORED);
+    b int GENERATED ALWAYS AS (num_nulls(gtest_err_2c)) VIRTUAL);
 ERROR:  cannot use whole-row variable in column generation expression
-LINE 2:     b int GENERATED ALWAYS AS (num_nulls(gtest_err_2c)) STOR...
+LINE 2:     b int GENERATED ALWAYS AS (num_nulls(gtest_err_2c)) VIRT...
                                                  ^
 DETAIL:  This would cause the generated column to depend on its own value.
 -- invalid reference
-CREATE TABLE gtest_err_3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) STORED);
+CREATE TABLE gtest_err_3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) VIRTUAL);
 ERROR:  column "c" does not exist
-LINE 1: ..._3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) STO...
+LINE 1: ..._3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) VIR...
                                                              ^
 -- generation expression must be immutable
-CREATE TABLE gtest_err_4 (a int PRIMARY KEY, b double precision GENERATED ALWAYS AS (random()) STORED);
+CREATE TABLE gtest_err_4 (a int PRIMARY KEY, b double precision GENERATED ALWAYS AS (random()) VIRTUAL);
 ERROR:  generation expression is not immutable
 -- ... but be sure that the immutability test is accurate
-CREATE TABLE gtest2 (a int, b text GENERATED ALWAYS AS (a || ' sec') STORED);
+CREATE TABLE gtest2 (a int, b text GENERATED ALWAYS AS (a || ' sec') VIRTUAL);
 DROP TABLE gtest2;
 -- 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_5a (a int PRIMARY KEY, b int DEFAULT 5 GENERATED ALWAYS AS (a * 2) VIRTUAL);
 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);
+CREATE TABLE gtest_err_5b (a int PRIMARY KEY, b int GENERATED ALWAYS AS identity GENERATED ALWAYS AS (a * 2) VIRTUAL);
 ERROR:  both identity and generation expression specified for column "b" of table "gtest_err_5b"
 LINE 1: ...t PRIMARY KEY, b int GENERATED ALWAYS AS identity GENERATED ...
                                                              ^
 -- reference to system column not allowed in generated column
 -- (except tableoid, which we test below)
-CREATE TABLE gtest_err_6a (a int PRIMARY KEY, b bool GENERATED ALWAYS AS (xmin <> 37) STORED);
+CREATE TABLE gtest_err_6a (a int PRIMARY KEY, b bool GENERATED ALWAYS AS (xmin <> 37) VIRTUAL);
 ERROR:  cannot use system column "xmin" in column generation expression
 LINE 1: ...a (a int PRIMARY KEY, b bool GENERATED ALWAYS AS (xmin <> 37...
                                                              ^
 -- various prohibited constructs
-CREATE TABLE gtest_err_7a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (avg(a)) STORED);
+CREATE TABLE gtest_err_7a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (avg(a)) VIRTUAL);
 ERROR:  aggregate functions are not allowed in column generation expressions
-LINE 1: ...7a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (avg(a)) ST...
+LINE 1: ...7a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (avg(a)) VI...
                                                              ^
-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_7b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (row_number() OVER (ORDER BY a)) VIRTUAL);
 ERROR:  window functions are not allowed in column generation expressions
 LINE 1: ...7b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (row_number...
                                                              ^
-CREATE TABLE gtest_err_7c (a int PRIMARY KEY, b int GENERATED ALWAYS AS ((SELECT a)) STORED);
+CREATE TABLE gtest_err_7c (a int PRIMARY KEY, b int GENERATED ALWAYS AS ((SELECT a)) VIRTUAL);
 ERROR:  cannot use subquery in column generation expression
 LINE 1: ...7c (a int PRIMARY KEY, b int GENERATED ALWAYS AS ((SELECT a)...
                                                              ^
-CREATE TABLE gtest_err_7d (a int PRIMARY KEY, b int GENERATED ALWAYS AS (generate_series(1, a)) STORED);
+CREATE TABLE gtest_err_7d (a int PRIMARY KEY, b int GENERATED ALWAYS AS (generate_series(1, a)) VIRTUAL);
 ERROR:  set-returning functions are not allowed in column generation expressions
 LINE 1: ...7d (a int PRIMARY KEY, b int GENERATED ALWAYS AS (generate_s...
                                                              ^
 -- GENERATED BY DEFAULT not allowed
-CREATE TABLE gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT AS (a * 2) STORED);
+CREATE TABLE gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT AS (a * 2) VIRTUAL);
 ERROR:  for a generated column, GENERATED ALWAYS must be specified
 LINE 1: ...E gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT...
                                                              ^
@@ -153,16 +153,10 @@ SELECT a, b FROM gtest1 WHERE b = 4 ORDER BY a;
  2 | 4
 (1 row)
 
--- test that overflow error happens on write
+-- test that overflow error happens on read
 INSERT INTO gtest1 VALUES (2000000000);
-ERROR:  integer out of range
 SELECT * FROM gtest1;
- a | b 
----+---
- 2 | 4
- 1 | 2
-(2 rows)
-
+ERROR:  integer out of range
 DELETE FROM gtest1 WHERE a = 2000000000;
 -- test with joins
 CREATE TABLE gtestx (x int, y int);
@@ -203,8 +197,8 @@ CREATE TABLE gtestm (
   id int PRIMARY KEY,
   f1 int,
   f2 int,
-  f3 int GENERATED ALWAYS AS (f1 * 2) STORED,
-  f4 int GENERATED ALWAYS AS (f2 * 2) STORED
+  f3 int GENERATED ALWAYS AS (f1 * 2) VIRTUAL,
+  f4 int GENERATED ALWAYS AS (f2 * 2) VIRTUAL
 );
 INSERT INTO gtestm VALUES (1, 5, 100);
 MERGE INTO gtestm t USING (VALUES (1, 10), (2, 20)) v(id, f1) ON t.id = v.id
@@ -217,6 +211,27 @@ SELECT * FROM gtestm ORDER BY id;
   2 | 20 | 200 | 40 | 400
 (2 rows)
 
+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
+  b int GENERATED ALWAYS AS (a * 2) VIRTUAL
+);
+INSERT INTO gtestm (a) SELECT g FROM generate_series(1, 10) g;
+MERGE INTO gtestm t USING gtestm AS s ON 2 * t.a = s.b WHEN MATCHED THEN DELETE RETURNING *;
+ a  | b  | a  | b  
+----+----+----+----
+  1 |  2 |  1 |  2
+  2 |  4 |  2 |  4
+  3 |  6 |  3 |  6
+  4 |  8 |  4 |  8
+  5 | 10 |  5 | 10
+  6 | 12 |  6 | 12
+  7 | 14 |  7 | 14
+  8 | 16 |  8 | 16
+  9 | 18 |  9 | 18
+ 10 | 20 | 10 | 20
+(10 rows)
+
 DROP TABLE gtestm;
 -- views
 CREATE VIEW gtest1v AS SELECT * FROM gtest1;
@@ -273,11 +288,11 @@ SELECT * FROM gtest1_1;
 (0 rows)
 
 \d gtest1_1
-                   Table "generated_stored_tests.gtest1_1"
- Column |  Type   | Collation | Nullable |              Default               
---------+---------+-----------+----------+------------------------------------
+               Table "generated_virtual_tests.gtest1_1"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
  a      | integer |           | not null | 
- b      | integer |           |          | generated always as (a * 2) stored
+ b      | integer |           |          | generated always as (a * 2)
 Inherits: gtest1
 
 INSERT INTO gtest1_1 VALUES (4);
@@ -296,12 +311,12 @@ SELECT * FROM gtest1;
 
 -- can't have generated column that is a child of normal column
 CREATE TABLE gtest_normal (a int, b int);
-CREATE TABLE gtest_normal_child (a int, b int GENERATED ALWAYS AS (a * 2) STORED) INHERITS (gtest_normal);  -- error
+CREATE TABLE gtest_normal_child (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL) INHERITS (gtest_normal);  -- error
 NOTICE:  merging column "a" with inherited definition
 NOTICE:  merging column "b" with inherited definition
 ERROR:  child column "b" specifies generation expression
 HINT:  A child table column cannot be generated unless its parent column is.
-CREATE TABLE gtest_normal_child (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE gtest_normal_child (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 ALTER TABLE gtest_normal_child INHERIT gtest_normal;  -- error
 ERROR:  column "b" in child table must not be a generated column
 DROP TABLE gtest_normal, gtest_normal_child;
@@ -312,23 +327,42 @@ ERROR:  column "b" inherits from generated column but specifies default
 CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS IDENTITY) INHERITS (gtest1);  -- error
 NOTICE:  merging column "b" with inherited definition
 ERROR:  column "b" inherits from generated column but specifies identity
-CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) STORED) INHERITS (gtest1);  -- ok, overrides parent
+CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) STORED) INHERITS (gtest1);  -- error
+NOTICE:  merging column "b" with inherited definition
+ERROR:  column "b" inherits from generated column of different kind
+DETAIL:  Parent column is VIRTUAL, child column is STORED.
+CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) VIRTUAL) INHERITS (gtest1);  -- ok, overrides parent
 NOTICE:  merging column "b" with inherited definition
 \d+ gtestx
-                                        Table "generated_stored_tests.gtestx"
- Column |  Type   | Collation | Nullable |               Default               | Storage | Stats target | Description 
---------+---------+-----------+----------+-------------------------------------+---------+--------------+-------------
- a      | integer |           | not null |                                     | plain   |              | 
- b      | integer |           |          | generated always as (a * 22) stored | plain   |              | 
- x      | integer |           |          |                                     | plain   |              | 
+                                    Table "generated_virtual_tests.gtestx"
+ Column |  Type   | Collation | Nullable |           Default            | Storage | Stats target | Description 
+--------+---------+-----------+----------+------------------------------+---------+--------------+-------------
+ a      | integer |           | not null |                              | plain   |              | 
+ b      | integer |           |          | generated always as (a * 22) | plain   |              | 
+ x      | integer |           |          |                              | plain   |              | 
 Inherits: gtest1
 
+INSERT INTO gtestx (a, x) VALUES (11, 22);
+SELECT * FROM gtest1;
+ a  | b  
+----+----
+  3 |  6
+  4 |  8
+ 11 | 22
+(3 rows)
+
+SELECT * FROM gtestx;
+ a  |  b  | x  
+----+-----+----
+ 11 | 242 | 22
+(1 row)
+
 CREATE TABLE gtestxx_1 (a int NOT NULL, b int);
 ALTER TABLE gtestxx_1 INHERIT gtest1;  -- error
 ERROR:  column "b" in child table must be a generated column
-CREATE TABLE gtestxx_3 (a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE gtestxx_3 (a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 ALTER TABLE gtestxx_3 INHERIT gtest1;  -- ok
-CREATE TABLE gtestxx_4 (b int GENERATED ALWAYS AS (a * 2) STORED, a int NOT NULL);
+CREATE TABLE gtestxx_4 (b int GENERATED ALWAYS AS (a * 2) VIRTUAL, a int NOT NULL);
 ALTER TABLE gtestxx_4 INHERIT gtest1;  -- ok
 -- test multiple inheritance mismatches
 CREATE TABLE gtesty (x int, b int DEFAULT 55);
@@ -341,28 +375,28 @@ CREATE TABLE gtest1_y () INHERITS (gtest1, gtesty);  -- error
 NOTICE:  merging multiple inherited definitions of column "b"
 ERROR:  inherited column "b" has a generation conflict
 DROP TABLE gtesty;
-CREATE TABLE gtesty (x int, b int GENERATED ALWAYS AS (x * 22) STORED);
+CREATE TABLE gtesty (x int, b int GENERATED ALWAYS AS (x * 22) VIRTUAL);
 CREATE TABLE gtest1_y () INHERITS (gtest1, gtesty);  -- error
 NOTICE:  merging multiple inherited definitions of column "b"
 ERROR:  column "b" inherits conflicting generation expressions
 HINT:  To resolve the conflict, specify a generation expression explicitly.
-CREATE TABLE gtest1_y (b int GENERATED ALWAYS AS (x + 1) STORED) INHERITS (gtest1, gtesty);  -- ok
+CREATE TABLE gtest1_y (b int GENERATED ALWAYS AS (x + 1) VIRTUAL) INHERITS (gtest1, gtesty);  -- ok
 NOTICE:  merging multiple inherited definitions of column "b"
 NOTICE:  moving and merging column "b" with inherited definition
 DETAIL:  User-specified column moved to the position of the inherited column.
 \d gtest1_y
-                   Table "generated_stored_tests.gtest1_y"
- Column |  Type   | Collation | Nullable |              Default               
---------+---------+-----------+----------+------------------------------------
+               Table "generated_virtual_tests.gtest1_y"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
  a      | integer |           | not null | 
- b      | integer |           |          | generated always as (x + 1) stored
+ b      | integer |           |          | generated always as (x + 1)
  x      | integer |           |          | 
 Inherits: gtest1,
           gtesty
 
 -- test correct handling of GENERATED column that's only in child
 CREATE TABLE gtestp (f1 int);
-CREATE TABLE gtestc (f2 int GENERATED ALWAYS AS (f1+1) STORED) INHERITS(gtestp);
+CREATE TABLE gtestc (f2 int GENERATED ALWAYS AS (f1+1) VIRTUAL) INHERITS(gtestp);
 INSERT INTO gtestc values(42);
 TABLE gtestc;
  f1 | f2 
@@ -379,8 +413,8 @@ TABLE gtestc;
 
 DROP TABLE gtestp CASCADE;
 NOTICE:  drop cascades to table gtestc
--- test stored update
-CREATE TABLE gtest3 (a int, b int GENERATED ALWAYS AS (a * 3) STORED);
+-- test update XXX
+CREATE TABLE gtest3 (a int, b int GENERATED ALWAYS AS (a * 3) VIRTUAL);
 INSERT INTO gtest3 (a) VALUES (1), (2), (3), (NULL);
 SELECT * FROM gtest3 ORDER BY a;
  a | b 
@@ -401,7 +435,7 @@ SELECT * FROM gtest3 ORDER BY a;
     |   
 (4 rows)
 
-CREATE TABLE gtest3a (a text, b text GENERATED ALWAYS AS (a || '+' || a) STORED);
+CREATE TABLE gtest3a (a text, b text GENERATED ALWAYS AS (a || '+' || a) VIRTUAL);
 INSERT INTO gtest3a (a) VALUES ('a'), ('b'), ('c'), (NULL);
 SELECT * FROM gtest3a ORDER BY a;
  a |  b  
@@ -466,7 +500,7 @@ SELECT * FROM gtest3 ORDER BY a;
 (4 rows)
 
 -- null values
-CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED);
+CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) VIRTUAL);
 INSERT INTO gtest2 VALUES (1);
 SELECT * FROM gtest2;
  a | b 
@@ -475,7 +509,7 @@ SELECT * FROM gtest2;
 (1 row)
 
 -- simple column reference for varlena types
-CREATE TABLE gtest_varlena (a varchar, b varchar GENERATED ALWAYS AS (a) STORED);
+CREATE TABLE gtest_varlena (a varchar, b varchar GENERATED ALWAYS AS (a) VIRTUAL);
 INSERT INTO gtest_varlena (a) VALUES('01234567890123456789');
 INSERT INTO gtest_varlena (a) VALUES(NULL);
 SELECT * FROM gtest_varlena ORDER BY a;
@@ -490,7 +524,7 @@ DROP TABLE gtest_varlena;
 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
+    b double_int GENERATED ALWAYS AS ((a * 2, a * 3)) VIRTUAL
 );
 INSERT INTO gtest4 VALUES (1), (6);
 SELECT * FROM gtest4;
@@ -505,11 +539,11 @@ DROP TYPE double_int;
 -- using tableoid is allowed
 CREATE TABLE gtest_tableoid (
   a int PRIMARY KEY,
-  b bool GENERATED ALWAYS AS (tableoid = 'gtest_tableoid'::regclass) STORED
+  b bool GENERATED ALWAYS AS (tableoid = 'gtest_tableoid'::regclass) VIRTUAL
 );
 INSERT INTO gtest_tableoid VALUES (1), (2);
 ALTER TABLE gtest_tableoid ADD COLUMN
-  c regclass GENERATED ALWAYS AS (tableoid) STORED;
+  c regclass GENERATED ALWAYS AS (tableoid) VIRTUAL;
 SELECT * FROM gtest_tableoid;
  a | b |       c        
 ---+---+----------------
@@ -518,7 +552,7 @@ SELECT * FROM gtest_tableoid;
 (2 rows)
 
 -- drop column behavior
-CREATE TABLE gtest10 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) STORED);
+CREATE TABLE gtest10 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) VIRTUAL);
 ALTER TABLE gtest10 DROP COLUMN b;  -- fails
 ERROR:  cannot drop column b of table gtest10 because other objects depend on it
 DETAIL:  column c of table gtest10 depends on column b of table gtest10
@@ -526,30 +560,31 @@ HINT:  Use DROP ... CASCADE to drop the dependent objects too.
 ALTER TABLE gtest10 DROP COLUMN b CASCADE;  -- drops c too
 NOTICE:  drop cascades to column c of table gtest10
 \d gtest10
-      Table "generated_stored_tests.gtest10"
+      Table "generated_virtual_tests.gtest10"
  Column |  Type   | Collation | Nullable | Default 
 --------+---------+-----------+----------+---------
  a      | integer |           | not null | 
 Indexes:
     "gtest10_pkey" PRIMARY KEY, btree (a)
 
-CREATE TABLE gtest10a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE gtest10a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 ALTER TABLE gtest10a DROP COLUMN b;
 INSERT INTO gtest10a (a) VALUES (1);
 -- privileges
 CREATE USER regress_user11;
-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 TABLE gtest11v (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) VIRTUAL);
+INSERT INTO gtest11v VALUES (1, 10), (2, 20);
+GRANT SELECT (a, c) ON gtest11v 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);
+CREATE TABLE gtest12v (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) VIRTUAL);
+INSERT INTO gtest12v VALUES (1, 10), (2, 20);
 GRANT SELECT (a, c) ON gtest12s TO regress_user11;
+ERROR:  relation "gtest12s" does not exist
 SET ROLE regress_user11;
-SELECT a, b FROM gtest11s;  -- not allowed
-ERROR:  permission denied for table gtest11s
-SELECT a, c FROM gtest11s;  -- allowed
+SELECT a, b FROM gtest11v;  -- not allowed
+ERROR:  permission denied for table gtest11v
+SELECT a, c FROM gtest11v;  -- allowed
  a | c  
 ---+----
  1 | 20
@@ -558,231 +593,142 @@ SELECT a, c FROM gtest11s;  -- allowed
 
 SELECT gf1(10);  -- not allowed
 ERROR:  permission denied for function gf1
-SELECT a, c FROM gtest12s;  -- allowed
- a | c  
----+----
- 1 | 30
- 2 | 60
-(2 rows)
-
+SELECT a, c FROM gtest12v;  -- not allowed; TODO: ought to be allowed
+ERROR:  permission denied for table gtest12v
 RESET ROLE;
 DROP FUNCTION gf1(int);  -- fail
 ERROR:  cannot drop function gf1(integer) because other objects depend on it
-DETAIL:  column c of table gtest12s depends on function gf1(integer)
+DETAIL:  column c of table gtest12v depends on function gf1(integer)
 HINT:  Use DROP ... CASCADE to drop the dependent objects too.
-DROP TABLE gtest11s, gtest12s;
+DROP TABLE gtest11v, gtest12v;
 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));
+CREATE TABLE gtest20 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL CHECK (b < 50));
 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).
+DETAIL:  Failing row contains (30, virtual).
 ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);  -- violates constraint
-ERROR:  check constraint "gtest20_b_check" of relation "gtest20" is violated by some row
+ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns
+DETAIL:  Column "b" of relation "gtest20" is a virtual generated column.
 ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);  -- ok
-CREATE TABLE gtest20a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns
+DETAIL:  Column "b" of relation "gtest20" is a virtual generated column.
+CREATE TABLE gtest20a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 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" of relation "gtest20a" is violated by some row
-CREATE TABLE gtest20b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE gtest20b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 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" of relation "gtest20b" is violated by some row
--- 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
-ERROR:  null value in column "b" of relation "gtest21a" 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);
+-- not-null constraints (currently not supported)
+CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL);
+ERROR:  not-null constraints are not supported on virtual generated columns
+--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)) VIRTUAL);
 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" of relation "gtest21b" violates not-null constraint
-DETAIL:  Failing row contains (0, null).
+ERROR:  not-null constraints are not supported on virtual generated columns
+DETAIL:  Column "b" of relation "gtest21b" is a virtual generated column.
+--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
+--INSERT INTO gtest21b (a) VALUES (0);  -- ok now
 -- 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);
-ERROR:  duplicate key value violates unique constraint "gtest22a_b_key"
-DETAIL:  Key (b)=(1) already exists.
-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);
-ERROR:  duplicate key value violates unique constraint "gtest22b_pkey"
-DETAIL:  Key (a, b)=(2, 1) already exists.
+CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) VIRTUAL UNIQUE);
+ERROR:  index creation on virtual generated columns is not supported
+--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) VIRTUAL, PRIMARY KEY (a, b));
+ERROR:  index creation on virtual generated columns is not supported
+--INSERT INTO gtest22b VALUES (2);
+--INSERT INTO gtest22b VALUES (2);
 -- 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 "generated_stored_tests.gtest22c"
- Column |  Type   | Collation | Nullable |              Default               
---------+---------+-----------+----------+------------------------------------
- a      | integer |           |          | 
- b      | integer |           |          | generated always as (a * 2) stored
-Indexes:
-    "gtest22c_b_idx" btree (b)
-    "gtest22c_expr_idx" btree ((b * 3))
-    "gtest22c_pred_idx" btree (a) 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                  
----------------------------------------------
- Index Scan using gtest22c_b_idx on gtest22c
-   Index Cond: (b = 4)
-(2 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                   
-------------------------------------------------
- Index Scan using gtest22c_expr_idx on gtest22c
-   Index Cond: ((b * 3) = 6)
-(2 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                   
-------------------------------------------------
- Index Scan using gtest22c_pred_idx on gtest22c
-   Index Cond: (a = 1)
-(2 rows)
-
-SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
- a | b 
----+---
- 1 | 2
-(1 row)
-
-ALTER TABLE gtest22c ALTER COLUMN b SET EXPRESSION AS (a * 4);
-ANALYZE gtest22c;
-EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 8;
-                 QUERY PLAN                  
----------------------------------------------
- Index Scan using gtest22c_b_idx on gtest22c
-   Index Cond: (b = 8)
-(2 rows)
-
-SELECT * FROM gtest22c WHERE b = 8;
- a | b 
----+---
- 2 | 8
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 12;
-                   QUERY PLAN                   
-------------------------------------------------
- Index Scan using gtest22c_expr_idx on gtest22c
-   Index Cond: ((b * 3) = 12)
-(2 rows)
-
-SELECT * FROM gtest22c WHERE b * 3 = 12;
- a | b 
----+---
- 1 | 4
-(1 row)
-
-EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
-                   QUERY PLAN                   
-------------------------------------------------
- Index Scan using gtest22c_pred_idx on gtest22c
-   Index Cond: (a = 1)
-(2 rows)
-
-SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
- a | b 
----+---
- 1 | 4
-(1 row)
-
-RESET enable_seqscan;
-RESET enable_bitmapscan;
+CREATE TABLE gtest22c (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
+--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;
+--ALTER TABLE gtest22c ALTER COLUMN b SET EXPRESSION AS (a * 4);
+--ANALYZE gtest22c;
+--EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 8;
+--SELECT * FROM gtest22c WHERE b = 8;
+--EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 12;
+--SELECT * FROM gtest22c WHERE b * 3 = 12;
+--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;
 -- 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
+--INSERT INTO gtest23a VALUES (1, 11), (2, 22), (3, 33);
+CREATE TABLE gtest23x (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL REFERENCES gtest23a (x) ON UPDATE CASCADE);  -- error
 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
+CREATE TABLE gtest23x (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL REFERENCES gtest23a (x) ON DELETE SET NULL);  -- error
 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));
-\d gtest23b
-                   Table "generated_stored_tests.gtest23b"
- Column |  Type   | Collation | Nullable |              Default               
---------+---------+-----------+----------+------------------------------------
- a      | integer |           | not null | 
- b      | integer |           |          | generated always as (a * 2) stored
-Indexes:
-    "gtest23b_pkey" PRIMARY KEY, btree (a)
-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".
-ALTER TABLE gtest23b ALTER COLUMN b SET EXPRESSION AS (a * 5); -- error
-ERROR:  insert or update on table "gtest23b" violates foreign key constraint "gtest23b_b_fkey"
-DETAIL:  Key (b)=(5) is not present in table "gtest23a".
-ALTER TABLE gtest23b ALTER COLUMN b SET EXPRESSION AS (a * 1); -- ok
-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 gtest23b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL REFERENCES gtest23a (x));
+ERROR:  foreign key constraints on virtual generated columns are not supported
+--\d gtest23b
+--INSERT INTO gtest23b VALUES (1);  -- ok
+--INSERT INTO gtest23b VALUES (5);  -- error
+--ALTER TABLE gtest23b ALTER COLUMN b SET EXPRESSION AS (a * 5); -- error
+--ALTER TABLE gtest23b ALTER COLUMN b SET EXPRESSION AS (a * 1); -- ok
+--DROP TABLE gtest23b;
+--DROP TABLE gtest23a;
+CREATE TABLE gtest23p (x int, y int GENERATED ALWAYS AS (x * 2) VIRTUAL, PRIMARY KEY (y));
+ERROR:  index creation on virtual generated columns is not supported
+--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
-ERROR:  insert or update on table "gtest23q" violates foreign key constraint "gtest23q_b_fkey"
-DETAIL:  Key (b)=(5) is not present in table "gtest23p".
+ERROR:  relation "gtest23p" does not exist
+--INSERT INTO gtest23q VALUES (1, 2);  -- ok
+--INSERT INTO gtest23q VALUES (2, 5);  -- error
 -- 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
-ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a * 2) VIRTUAL);
+ERROR:  virtual generated column "b" cannot have a domain type
+--INSERT INTO gtest24 (a) VALUES (4);  -- ok
+--INSERT INTO gtest24 (a) VALUES (6);  -- error
+CREATE TYPE gtestdomain1range AS range (subtype = gtestdomain1);
+CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) VIRTUAL);
+ERROR:  virtual generated column "b" cannot have a domain type
+--INSERT INTO gtest24r (a) VALUES (4);  -- ok
+--INSERT INTO gtest24r (a) VALUES (6);  -- error
 -- 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);
+CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) VIRTUAL);
 ERROR:  generated columns are not supported on typed tables
 DROP TYPE gtest_type CASCADE;
 -- partitioning cases
 CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint) PARTITION BY RANGE (f1);
 CREATE TABLE gtest_child PARTITION OF gtest_parent (
-    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 2) STORED
+    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 2) VIRTUAL
 ) FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- error
 ERROR:  child column "f3" specifies generation expression
 HINT:  A child table column cannot be generated unless its parent column is.
-CREATE TABLE gtest_child (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED);
+CREATE TABLE gtest_child (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- error
 ERROR:  column "f3" in child table must not be a generated column
 DROP TABLE gtest_parent, gtest_child;
-CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f1);
+CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE (f1);
 CREATE TABLE gtest_child PARTITION OF gtest_parent
   FOR VALUES FROM ('2016-07-01') TO ('2016-08-01');  -- inherits gen expr
 CREATE TABLE gtest_child2 PARTITION OF gtest_parent (
-    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 22) STORED  -- overrides gen expr
+    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 22) VIRTUAL  -- overrides gen expr
 ) FOR VALUES FROM ('2016-08-01') TO ('2016-09-01');
 CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
     f3 DEFAULT 42  -- error
@@ -792,6 +738,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
     f3 WITH OPTIONS GENERATED ALWAYS AS IDENTITY  -- error
 ) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 ERROR:  identity columns are not supported on partitions
+CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
+    f3 GENERATED ALWAYS AS (f2 * 2) STORED  -- error
+) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
+ERROR:  column "f3" inherits from generated column of different kind
+DETAIL:  Parent column is VIRTUAL, child column is STORED.
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
 ERROR:  column "f3" in child table must be a generated column
@@ -805,33 +756,33 @@ ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09
 ERROR:  table "gtest_child3" being attached contains an identity column "f3"
 DETAIL:  The new partition may not contain an identity column.
 DROP TABLE gtest_child3;
-CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) STORED);
+CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) VIRTUAL);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 \d gtest_child
-                  Table "generated_stored_tests.gtest_child"
- Column |  Type  | Collation | Nullable |               Default               
---------+--------+-----------+----------+-------------------------------------
+              Table "generated_virtual_tests.gtest_child"
+ Column |  Type  | Collation | Nullable |           Default            
+--------+--------+-----------+----------+------------------------------
  f1     | date   |           | not null | 
  f2     | bigint |           |          | 
- f3     | bigint |           |          | generated always as (f2 * 2) stored
+ f3     | bigint |           |          | generated always as (f2 * 2)
 Partition of: gtest_parent FOR VALUES FROM ('07-01-2016') TO ('08-01-2016')
 
 \d gtest_child2
-                  Table "generated_stored_tests.gtest_child2"
- Column |  Type  | Collation | Nullable |               Default                
---------+--------+-----------+----------+--------------------------------------
+              Table "generated_virtual_tests.gtest_child2"
+ Column |  Type  | Collation | Nullable |            Default            
+--------+--------+-----------+----------+-------------------------------
  f1     | date   |           | not null | 
  f2     | bigint |           |          | 
- f3     | bigint |           |          | generated always as (f2 * 22) stored
+ f3     | bigint |           |          | generated always as (f2 * 22)
 Partition of: gtest_parent FOR VALUES FROM ('08-01-2016') TO ('09-01-2016')
 
 \d gtest_child3
-                  Table "generated_stored_tests.gtest_child3"
- Column |  Type  | Collation | Nullable |               Default                
---------+--------+-----------+----------+--------------------------------------
+              Table "generated_virtual_tests.gtest_child3"
+ Column |  Type  | Collation | Nullable |            Default            
+--------+--------+-----------+----------+-------------------------------
  f1     | date   |           | not null | 
  f2     | bigint |           |          | 
- f3     | bigint |           |          | generated always as (f2 * 33) stored
+ f3     | bigint |           |          | generated always as (f2 * 33)
 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);
@@ -842,7 +793,7 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
 --------------+------------+----+----
  gtest_child  | 07-15-2016 |  1 |  2
  gtest_child  | 07-15-2016 |  2 |  4
- gtest_child2 | 08-15-2016 |  3 | 66
+ gtest_child2 | 08-15-2016 |  3 |  6
 (3 rows)
 
 UPDATE gtest_parent SET f1 = f1 + 60 WHERE f2 = 1;
@@ -850,95 +801,101 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
    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
+ gtest_child2 | 08-15-2016 |  3 |  6
+ gtest_child3 | 09-13-2016 |  1 |  2
 (3 rows)
 
 -- alter only parent's and one child's generation expression
 ALTER TABLE ONLY gtest_parent ALTER COLUMN f3 SET EXPRESSION AS (f2 * 4);
+ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns
+DETAIL:  Column "f3" of relation "gtest_parent" is a virtual generated column.
 ALTER TABLE gtest_child ALTER COLUMN f3 SET EXPRESSION AS (f2 * 10);
+ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns
+DETAIL:  Column "f3" of relation "gtest_child" is a virtual generated column.
 \d gtest_parent
-           Partitioned table "generated_stored_tests.gtest_parent"
- Column |  Type  | Collation | Nullable |               Default               
---------+--------+-----------+----------+-------------------------------------
+       Partitioned table "generated_virtual_tests.gtest_parent"
+ Column |  Type  | Collation | Nullable |           Default            
+--------+--------+-----------+----------+------------------------------
  f1     | date   |           | not null | 
  f2     | bigint |           |          | 
- f3     | bigint |           |          | generated always as (f2 * 4) stored
+ f3     | bigint |           |          | generated always as (f2 * 2)
 Partition key: RANGE (f1)
 Number of partitions: 3 (Use \d+ to list them.)
 
 \d gtest_child
-                  Table "generated_stored_tests.gtest_child"
- Column |  Type  | Collation | Nullable |               Default                
---------+--------+-----------+----------+--------------------------------------
+              Table "generated_virtual_tests.gtest_child"
+ Column |  Type  | Collation | Nullable |           Default            
+--------+--------+-----------+----------+------------------------------
  f1     | date   |           | not null | 
  f2     | bigint |           |          | 
- f3     | bigint |           |          | generated always as (f2 * 10) stored
+ f3     | bigint |           |          | generated always as (f2 * 2)
 Partition of: gtest_parent FOR VALUES FROM ('07-01-2016') TO ('08-01-2016')
 
 \d gtest_child2
-                  Table "generated_stored_tests.gtest_child2"
- Column |  Type  | Collation | Nullable |               Default                
---------+--------+-----------+----------+--------------------------------------
+              Table "generated_virtual_tests.gtest_child2"
+ Column |  Type  | Collation | Nullable |            Default            
+--------+--------+-----------+----------+-------------------------------
  f1     | date   |           | not null | 
  f2     | bigint |           |          | 
- f3     | bigint |           |          | generated always as (f2 * 22) stored
+ f3     | bigint |           |          | generated always as (f2 * 22)
 Partition of: gtest_parent FOR VALUES FROM ('08-01-2016') TO ('09-01-2016')
 
 \d gtest_child3
-                  Table "generated_stored_tests.gtest_child3"
- Column |  Type  | Collation | Nullable |               Default                
---------+--------+-----------+----------+--------------------------------------
+              Table "generated_virtual_tests.gtest_child3"
+ Column |  Type  | Collation | Nullable |            Default            
+--------+--------+-----------+----------+-------------------------------
  f1     | date   |           | not null | 
  f2     | bigint |           |          | 
- f3     | bigint |           |          | generated always as (f2 * 33) stored
+ f3     | bigint |           |          | generated always as (f2 * 33)
 Partition of: gtest_parent FOR VALUES FROM ('09-01-2016') TO ('10-01-2016')
 
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
    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
+ gtest_child  | 07-15-2016 |  2 |  4
+ gtest_child2 | 08-15-2016 |  3 |  6
+ gtest_child3 | 09-13-2016 |  1 |  2
 (3 rows)
 
 -- alter generation expression of parent and all its children altogether
 ALTER TABLE gtest_parent ALTER COLUMN f3 SET EXPRESSION AS (f2 * 2);
+ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns
+DETAIL:  Column "f3" of relation "gtest_parent" is a virtual generated column.
 \d gtest_parent
-           Partitioned table "generated_stored_tests.gtest_parent"
- Column |  Type  | Collation | Nullable |               Default               
---------+--------+-----------+----------+-------------------------------------
+       Partitioned table "generated_virtual_tests.gtest_parent"
+ Column |  Type  | Collation | Nullable |           Default            
+--------+--------+-----------+----------+------------------------------
  f1     | date   |           | not null | 
  f2     | bigint |           |          | 
- f3     | bigint |           |          | generated always as (f2 * 2) stored
+ f3     | bigint |           |          | generated always as (f2 * 2)
 Partition key: RANGE (f1)
 Number of partitions: 3 (Use \d+ to list them.)
 
 \d gtest_child
-                  Table "generated_stored_tests.gtest_child"
- Column |  Type  | Collation | Nullable |               Default               
---------+--------+-----------+----------+-------------------------------------
+              Table "generated_virtual_tests.gtest_child"
+ Column |  Type  | Collation | Nullable |           Default            
+--------+--------+-----------+----------+------------------------------
  f1     | date   |           | not null | 
  f2     | bigint |           |          | 
- f3     | bigint |           |          | generated always as (f2 * 2) stored
+ f3     | bigint |           |          | generated always as (f2 * 2)
 Partition of: gtest_parent FOR VALUES FROM ('07-01-2016') TO ('08-01-2016')
 
 \d gtest_child2
-                 Table "generated_stored_tests.gtest_child2"
- Column |  Type  | Collation | Nullable |               Default               
---------+--------+-----------+----------+-------------------------------------
+              Table "generated_virtual_tests.gtest_child2"
+ Column |  Type  | Collation | Nullable |            Default            
+--------+--------+-----------+----------+-------------------------------
  f1     | date   |           | not null | 
  f2     | bigint |           |          | 
- f3     | bigint |           |          | generated always as (f2 * 2) stored
+ f3     | bigint |           |          | generated always as (f2 * 22)
 Partition of: gtest_parent FOR VALUES FROM ('08-01-2016') TO ('09-01-2016')
 
 \d gtest_child3
-                 Table "generated_stored_tests.gtest_child3"
- Column |  Type  | Collation | Nullable |               Default               
---------+--------+-----------+----------+-------------------------------------
+              Table "generated_virtual_tests.gtest_child3"
+ Column |  Type  | Collation | Nullable |            Default            
+--------+--------+-----------+----------+-------------------------------
  f1     | date   |           | not null | 
  f2     | bigint |           |          | 
- f3     | bigint |           |          | generated always as (f2 * 2) stored
+ f3     | bigint |           |          | generated always as (f2 * 33)
 Partition of: gtest_parent FOR VALUES FROM ('09-01-2016') TO ('10-01-2016')
 
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
@@ -951,54 +908,54 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
 
 -- we leave these tables around for purposes of testing dump/reload/upgrade
 -- generated columns in partition key (not allowed)
-CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f3);
+CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE (f3);
 ERROR:  cannot use generated column in partition key
-LINE 1: ...ENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f3);
+LINE 1: ...NERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE (f3);
                                                                    ^
 DETAIL:  Column "f3" is a generated column.
-CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((f3 * 3));
+CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((f3 * 3));
 ERROR:  cannot use generated column in partition key
-LINE 1: ...ED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((f3 * 3));
+LINE 1: ...D ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((f3 * 3));
                                                              ^
 DETAIL:  Column "f3" is a generated column.
 -- 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 * 2) STORED, ALTER COLUMN b SET EXPRESSION AS (a * 3);
+ALTER TABLE gtest25 ADD COLUMN b int GENERATED ALWAYS AS (a * 2) VIRTUAL, ALTER COLUMN b SET EXPRESSION AS (a * 3);
+ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns
+DETAIL:  Column "b" of relation "gtest25" is a virtual generated column.
 SELECT * FROM gtest25 ORDER BY a;
- a | b  
----+----
- 3 |  9
- 4 | 12
+ a 
+---
+ 3
+ 4
 (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
+ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (b * 4) VIRTUAL;  -- error
+ERROR:  column "b" does not exist
+ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (z * 4) VIRTUAL;  -- error
 ERROR:  column "z" does not exist
 ALTER TABLE gtest25 ADD COLUMN c int DEFAULT 42,
-  ADD COLUMN x int GENERATED ALWAYS AS (c * 4) STORED;
+  ADD COLUMN x int GENERATED ALWAYS AS (c * 4) VIRTUAL;
 ALTER TABLE gtest25 ADD COLUMN d int DEFAULT 101;
 ALTER TABLE gtest25 ALTER COLUMN d SET DATA TYPE float8,
-  ADD COLUMN y float8 GENERATED ALWAYS AS (d * 4) STORED;
+  ADD COLUMN y float8 GENERATED ALWAYS AS (d * 4) VIRTUAL;
 SELECT * FROM gtest25 ORDER BY a;
- a | b  | c  |  x  |  d  |  y  
----+----+----+-----+-----+-----
- 3 |  9 | 42 | 168 | 101 | 404
- 4 | 12 | 42 | 168 | 101 | 404
+ a | c  |  x  |  d  |  y  
+---+----+-----+-----+-----
+ 3 | 42 | 168 | 101 | 404
+ 4 | 42 | 168 | 101 | 404
 (2 rows)
 
 \d gtest25
-                                 Table "generated_stored_tests.gtest25"
- Column |       Type       | Collation | Nullable |                       Default                        
---------+------------------+-----------+----------+------------------------------------------------------
+                             Table "generated_virtual_tests.gtest25"
+ Column |       Type       | Collation | Nullable |                    Default                    
+--------+------------------+-----------+----------+-----------------------------------------------
  a      | integer          |           | not null | 
- b      | integer          |           |          | generated always as (a * 3) stored
  c      | integer          |           |          | 42
- x      | integer          |           |          | generated always as (c * 4) stored
+ x      | integer          |           |          | generated always as (c * 4)
  d      | double precision |           |          | 101
- y      | double precision |           |          | generated always as (d * 4::double precision) stored
+ y      | double precision |           |          | generated always as (d * 4::double precision)
 Indexes:
     "gtest25_pkey" PRIMARY KEY, btree (a)
 
@@ -1006,7 +963,7 @@ Indexes:
 CREATE TABLE gtest27 (
     a int,
     b int,
-    x int GENERATED ALWAYS AS ((a + b) * 2) STORED
+    x int GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL
 );
 INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11);
 ALTER TABLE gtest27 ALTER COLUMN a TYPE text;  -- error
@@ -1014,12 +971,12 @@ ERROR:  cannot alter type of a column used by a generated column
 DETAIL:  Column "a" is used by generated column "x".
 ALTER TABLE gtest27 ALTER COLUMN x TYPE numeric;
 \d gtest27
-                        Table "generated_stored_tests.gtest27"
- Column |  Type   | Collation | Nullable |                  Default                   
---------+---------+-----------+----------+--------------------------------------------
+                    Table "generated_virtual_tests.gtest27"
+ Column |  Type   | Collation | Nullable |               Default               
+--------+---------+-----------+----------+-------------------------------------
  a      | integer |           |          | 
  b      | integer |           |          | 
- x      | numeric |           |          | generated always as (((a + b) * 2)) stored
+ x      | numeric |           |          | generated always as (((a + b) * 2))
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1033,20 +990,19 @@ ERROR:  cannot specify USING when altering type of generated column
 DETAIL:  Column "x" is a generated column.
 ALTER TABLE gtest27 ALTER COLUMN x DROP DEFAULT;  -- error
 ERROR:  column "x" of relation "gtest27" is a generated column
-HINT:  Use ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION instead.
 -- It's possible to alter the column types this way:
 ALTER TABLE gtest27
   DROP COLUMN x,
   ALTER COLUMN a TYPE bigint,
   ALTER COLUMN b TYPE bigint,
-  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) STORED;
+  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL;
 \d gtest27
-                      Table "generated_stored_tests.gtest27"
- Column |  Type  | Collation | Nullable |                 Default                  
---------+--------+-----------+----------+------------------------------------------
+                  Table "generated_virtual_tests.gtest27"
+ Column |  Type  | Collation | Nullable |              Default              
+--------+--------+-----------+----------+-----------------------------------
  a      | bigint |           |          | 
  b      | bigint |           |          | 
- x      | bigint |           |          | generated always as ((a + b) * 2) stored
+ x      | bigint |           |          | generated always as ((a + b) * 2)
 
 -- Ideally you could just do this, but not today (and should x change type?):
 ALTER TABLE gtest27
@@ -1055,12 +1011,12 @@ ALTER TABLE gtest27
 ERROR:  cannot alter type of a column used by a generated column
 DETAIL:  Column "a" is used by generated column "x".
 \d gtest27
-                      Table "generated_stored_tests.gtest27"
- Column |  Type  | Collation | Nullable |                 Default                  
---------+--------+-----------+----------+------------------------------------------
+                  Table "generated_virtual_tests.gtest27"
+ Column |  Type  | Collation | Nullable |              Default              
+--------+--------+-----------+----------+-----------------------------------
  a      | bigint |           |          | 
  b      | bigint |           |          | 
- x      | bigint |           |          | generated always as ((a + b) * 2) stored
+ x      | bigint |           |          | generated always as ((a + b) * 2)
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1072,7 +1028,7 @@ SELECT * FROM gtest27;
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -1083,107 +1039,118 @@ SELECT * FROM gtest29;
 (2 rows)
 
 \d gtest29
-                    Table "generated_stored_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 * 2) stored
+ b      | integer |           |          | generated always as (a * 2)
 
 ALTER TABLE gtest29 ALTER COLUMN a SET EXPRESSION AS (a * 3);  -- error
 ERROR:  column "a" of relation "gtest29" is not a generated column
 ALTER TABLE gtest29 ALTER COLUMN a DROP EXPRESSION;  -- error
-ERROR:  column "a" of relation "gtest29" is not a stored generated column
+ERROR:  column "a" of relation "gtest29" is not a 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
+NOTICE:  column "a" of relation "gtest29" is not a generated column, skipping
 -- Change the expression
 ALTER TABLE gtest29 ALTER COLUMN b SET EXPRESSION AS (a * 3);
+ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns
+DETAIL:  Column "b" of relation "gtest29" is a virtual generated column.
 SELECT * FROM gtest29;
- a | b  
----+----
- 3 |  9
- 4 | 12
+ a | b 
+---+---
+ 3 | 6
+ 4 | 8
 (2 rows)
 
 \d gtest29
-                    Table "generated_stored_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) stored
+ b      | integer |           |          | generated always as (a * 2)
 
 ALTER TABLE gtest29 ALTER COLUMN b DROP EXPRESSION;
+ERROR:  ALTER TABLE / DROP EXPRESSION is not supported for virtual generated columns
+DETAIL:  Column "b" of relation "gtest29" is a virtual generated column.
 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;
  a | b  
 ---+----
- 3 |  9
- 4 | 12
- 5 |   
- 6 | 66
-(4 rows)
+ 3 |  6
+ 4 |  8
+ 5 | 10
+(3 rows)
 
 \d gtest29
-      Table "generated_stored_tests.gtest29"
- Column |  Type   | Collation | Nullable | Default 
---------+---------+-----------+----------+---------
+                Table "generated_virtual_tests.gtest29"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
  a      | integer |           |          | 
- b      | integer |           |          | 
+ b      | integer |           |          | generated always as (a * 2)
 
 -- check that dependencies between columns have also been removed
 ALTER TABLE gtest29 DROP COLUMN a;  -- should not drop b
+ERROR:  cannot drop column a of table gtest29 because other objects depend on it
+DETAIL:  column b of table gtest29 depends on column a of table gtest29
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
 \d gtest29
-      Table "generated_stored_tests.gtest29"
- Column |  Type   | Collation | Nullable | Default 
---------+---------+-----------+----------+---------
- b      | integer |           |          | 
+                Table "generated_virtual_tests.gtest29"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
+ a      | integer |           |          | 
+ b      | integer |           |          | generated always as (a * 2)
 
 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 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_stored_tests.gtest30"
- Column |  Type   | Collation | Nullable | Default 
---------+---------+-----------+----------+---------
+                Table "generated_virtual_tests.gtest30"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
  a      | integer |           |          | 
- b      | integer |           |          | 
+ b      | integer |           |          | generated always as (a * 2)
 Number of child tables: 1 (Use \d+ to list them.)
 
 \d gtest30_1
-     Table "generated_stored_tests.gtest30_1"
- Column |  Type   | Collation | Nullable | Default 
---------+---------+-----------+----------+---------
+               Table "generated_virtual_tests.gtest30_1"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
  a      | integer |           |          | 
- b      | integer |           |          | 
+ b      | integer |           |          | generated always as (a * 2)
 Inherits: gtest30
 
 DROP TABLE gtest30 CASCADE;
 NOTICE:  drop cascades to table gtest30_1
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE ONLY gtest30 ALTER COLUMN b DROP EXPRESSION;  -- error
 ERROR:  ALTER TABLE / DROP EXPRESSION must be applied to child tables too
 \d gtest30
-                    Table "generated_stored_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) stored
+ b      | integer |           |          | generated always as (a * 2)
 Number of child tables: 1 (Use \d+ to list them.)
 
 \d gtest30_1
-                   Table "generated_stored_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) stored
+ b      | integer |           |          | generated always as (a * 2)
 Inherits: gtest30
 
 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION;  -- error
@@ -1191,7 +1158,7 @@ ERROR:  cannot drop generation expression from inherited column
 -- triggers
 CREATE TABLE gtest26 (
     a int PRIMARY KEY,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE FUNCTION gtest_trigger_func() RETURNS trigger
   LANGUAGE plpgsql
@@ -1244,7 +1211,7 @@ CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26
   EXECUTE PROCEDURE gtest_trigger_func();
 INSERT INTO gtest26 (a) VALUES (-2), (0), (3);
 INFO:  gtest2: BEFORE: new = (-2,)
-INFO:  gtest4: AFTER: new = (-2,-4)
+INFO:  gtest4: AFTER: new = (-2,)
 SELECT * FROM gtest26 ORDER BY a;
  a  | b  
 ----+----
@@ -1254,12 +1221,12 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
 UPDATE gtest26 SET a = a * -2;
-INFO:  gtest1: BEFORE: old = (-2,-4)
+INFO:  gtest1: BEFORE: old = (-2,)
 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)
+INFO:  gtest3: AFTER: old = (-2,)
+INFO:  gtest3: AFTER: new = (4,)
+INFO:  gtest4: AFTER: old = (3,)
+INFO:  gtest4: AFTER: new = (-6,)
 SELECT * FROM gtest26 ORDER BY a;
  a  |  b  
 ----+-----
@@ -1269,8 +1236,8 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
 DELETE FROM gtest26 WHERE a = -6;
-INFO:  gtest1: BEFORE: old = (-6,-12)
-INFO:  gtest3: AFTER: old = (-6,-12)
+INFO:  gtest1: BEFORE: old = (-6,)
+INFO:  gtest3: AFTER: old = (-6,)
 SELECT * FROM gtest26 ORDER BY a;
  a | b 
 ---+---
@@ -1281,6 +1248,8 @@ SELECT * FROM gtest26 ORDER BY a;
 DROP TRIGGER gtest1 ON gtest26;
 DROP TRIGGER gtest2 ON gtest26;
 DROP TRIGGER gtest3 ON gtest26;
+-- check disallowed modification of virtual columns
+-- TODO
 -- Check that an UPDATE of "a" fires the trigger for UPDATE OF b, per
 -- SQL standard.
 CREATE FUNCTION gtest_trigger_func3() RETURNS trigger
@@ -1294,9 +1263,11 @@ $$;
 CREATE TRIGGER gtest11 BEFORE UPDATE OF b ON gtest26
   FOR EACH ROW
   EXECUTE PROCEDURE gtest_trigger_func3();
+ERROR:  virtual generated columns are not supported as trigger columns
+DETAIL:  Column "b" is a virtual generated column.
 UPDATE gtest26 SET a = 1 WHERE a = 0;
-NOTICE:  OK
 DROP TRIGGER gtest11 ON gtest26;
+ERROR:  trigger "gtest11" for table "gtest26" does not exist
 TRUNCATE gtest26;
 -- check that modifications of stored generated columns in triggers do
 -- not get propagated
@@ -1320,14 +1291,13 @@ CREATE TRIGGER gtest12_03 BEFORE UPDATE ON gtest26
   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: old = (1,)
 INFO:  gtest12_01: BEFORE: new = (11,)
-INFO:  gtest12_03: BEFORE: old = (1,2)
-INFO:  gtest12_03: BEFORE: new = (10,)
+ERROR:  trigger modified virtual generated column value
 SELECT * FROM gtest26 ORDER BY a;
- a  | b  
-----+----
- 10 | 20
+ a | b 
+---+---
+ 1 | 2
 (1 row)
 
 -- LIKE INCLUDING GENERATED and dropped column handling
@@ -1335,22 +1305,77 @@ CREATE TABLE gtest28a (
   a int,
   b int,
   c int,
-  x int GENERATED ALWAYS AS (b * 2) STORED
+  x int GENERATED ALWAYS AS (b * 2) VIRTUAL
 );
 ALTER TABLE gtest28a DROP COLUMN a;
 CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
 \d gtest28*
-                   Table "generated_stored_tests.gtest28a"
- Column |  Type   | Collation | Nullable |              Default               
---------+---------+-----------+----------+------------------------------------
+               Table "generated_virtual_tests.gtest28a"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
  b      | integer |           |          | 
  c      | integer |           |          | 
- x      | integer |           |          | generated always as (b * 2) stored
+ x      | integer |           |          | generated always as (b * 2)
 
-                   Table "generated_stored_tests.gtest28b"
- Column |  Type   | Collation | Nullable |              Default               
---------+---------+-----------+----------+------------------------------------
+               Table "generated_virtual_tests.gtest28b"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
  b      | integer |           |          | 
  c      | integer |           |          | 
- x      | integer |           |          | generated always as (b * 2) stored
+ x      | integer |           |          | generated always as (b * 2)
 
+-- TODO: extra tests to weave into the right places
+-- sublinks
+CREATE TABLE t2 (
+    a int,
+    b int GENERATED ALWAYS AS (a * 2),
+    d int DEFAULT 22
+);
+INSERT INTO t2 (a) SELECT g FROM generate_series(1, 10) g;
+SELECT a, b, d, (SELECT t2.b) FROM t2;
+ a  | b  | d  | b  
+----+----+----+----
+  1 |  2 | 22 |  2
+  2 |  4 | 22 |  4
+  3 |  6 | 22 |  6
+  4 |  8 | 22 |  8
+  5 | 10 | 22 | 10
+  6 | 12 | 22 | 12
+  7 | 14 | 22 | 14
+  8 | 16 | 22 | 16
+  9 | 18 | 22 | 18
+ 10 | 20 | 22 | 20
+(10 rows)
+
+DROP TABLE t2;
+-- collate
+/*
+CREATE COLLATION case_insensitive (provider = icu, locale = 'und-u-ks-level2', deterministic = false);
+CREATE TABLE t5 (
+    a text collate "C",
+    b text collate "C" GENERATED ALWAYS AS (a collate case_insensitive) ,
+    d int DEFAULT 22
+);
+INSERT INTO t5 (a, d) values ('d1', 28), ('D2', 27), ('D1', 26);
+SELECT * FROM t5 ORDER BY b ASC, d ASC;
+DROP TABLE t5;
+DROP COLLATION case_insensitive;
+*/
+-- composite type dependencies
+CREATE TABLE at_tab1 (a int, b text GENERATED ALWAYS AS ('hello'), c text);
+CREATE TABLE at_tab2 (x int, y at_tab1);
+ALTER TABLE at_tab1 ALTER COLUMN b TYPE varchar;  -- fails
+ERROR:  cannot alter table "at_tab1" because column "at_tab2.y" uses its row type
+DROP TABLE at_tab1, at_tab2;
+-- Check it for a partitioned table, too
+CREATE TABLE at_tab1 (a int, b text GENERATED ALWAYS AS ('hello'), c text) PARTITION BY LIST (a);
+CREATE TABLE at_tab2 (x int, y at_tab1);
+ALTER TABLE at_tab1 ALTER COLUMN b TYPE varchar;  -- fails
+ERROR:  cannot alter table "at_tab1" because column "at_tab2.y" uses its row type
+DROP TABLE at_tab1, at_tab2;
+-- attach mixing generated kinds
+CREATE TABLE bar1 (a integer, b integer GENERATED ALWAYS AS (22)) PARTITION BY RANGE (a);
+CREATE TABLE bar2 (a integer, b integer GENERATED ALWAYS AS (22) STORED);
+ALTER TABLE bar1 ATTACH PARTITION bar2 DEFAULT;  -- error
+ERROR:  column "b" inherits from generated column of different kind
+DETAIL:  Parent column is VIRTUAL, child column is STORED.
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 7a5a910562e..2e56537f194 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -68,6 +68,9 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated_stored join_hash
 
+# TODO: find a place for this (above group is full)
+test: generated_virtual
+
 # ----------
 # Additional BRIN tests
 # ----------
diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql
index 04008a027b8..ddf893f7ec3 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -51,7 +51,7 @@ CREATE TABLE test_like_id_3 (LIKE test_like_id_1 INCLUDING IDENTITY);
 SELECT * FROM test_like_id_3;  -- identity was copied and applied
 DROP TABLE test_like_id_1, test_like_id_2, test_like_id_3;
 
-CREATE TABLE test_like_gen_1 (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE test_like_gen_1 (a int, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (a * 3) VIRTUAL);
 \d test_like_gen_1
 INSERT INTO test_like_gen_1 (a) VALUES (1);
 SELECT * FROM test_like_gen_1;
diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql
index c18e0e1f655..36a6c2bddbe 100644
--- a/src/test/regress/sql/generated_stored.sql
+++ b/src/test/regress/sql/generated_stored.sql
@@ -1,5 +1,5 @@
 -- sanity check of system catalog
-SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v');

 CREATE SCHEMA generated_stored_tests;
@@ -103,6 +103,14 @@ CREATE TABLE gtestm (
 SELECT * FROM gtestm ORDER BY id;
 DROP TABLE gtestm;
 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
+  b int GENERATED ALWAYS AS (a * 2) STORED
+);
+INSERT INTO gtestm (a) SELECT g FROM generate_series(1, 10) g;
+MERGE INTO gtestm t USING gtestm AS s ON 2 * t.a = s.b WHEN MATCHED THEN DELETE RETURNING *;
+DROP TABLE gtestm;
+
 -- views
 CREATE VIEW gtest1v AS SELECT * FROM gtest1;
 SELECT * FROM gtest1v;
@@ -390,6 +398,10 @@ 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
+CREATE TYPE gtestdomain1range AS range (subtype = gtestdomain1);
+CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) STORED);
+INSERT INTO gtest24r (a) VALUES (4);  -- ok
+INSERT INTO gtest24r (a) VALUES (6);  -- error
 
 -- typed tables (currently not supported)
 CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint);
diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_virtual.sql
similarity index 69%
copy from src/test/regress/sql/generated_stored.sql
copy to src/test/regress/sql/generated_virtual.sql
index c18e0e1f655..34b8070b3e3 100644
--- a/src/test/regress/sql/generated_stored.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -1,55 +1,55 @@
 -- sanity check of system catalog
-SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v');

-CREATE SCHEMA generated_stored_tests;
-GRANT USAGE ON SCHEMA generated_stored_tests TO PUBLIC;
-SET search_path = generated_stored_tests;
+CREATE SCHEMA generated_virtual_tests;
+GRANT USAGE ON SCHEMA generated_virtual_tests TO PUBLIC;
+SET search_path = generated_virtual_tests;
 
-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);
+CREATE TABLE gtest0 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (55) VIRTUAL);
+CREATE TABLE gtest1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 
-SELECT table_name, column_name, column_default, is_nullable, is_generated, generation_expression FROM information_schema.columns WHERE table_schema = 'generated_stored_tests' ORDER BY 1, 2;
+SELECT table_name, column_name, column_default, is_nullable, is_generated, generation_expression FROM information_schema.columns WHERE table_schema = 'generated_virtual_tests' ORDER BY 1, 2;
 
-SELECT table_name, column_name, dependent_column FROM information_schema.column_column_usage WHERE table_schema = 'generated_stored_tests' ORDER BY 1, 2, 3;
+SELECT table_name, column_name, dependent_column FROM information_schema.column_column_usage WHERE table_schema = 'generated_virtual_tests' ORDER BY 1, 2, 3;
 
 \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);
+CREATE TABLE gtest_err_1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL GENERATED ALWAYS AS (a * 3) VIRTUAL);
 
 -- 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);
+CREATE TABLE gtest_err_2a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (b * 2) VIRTUAL);
+CREATE TABLE gtest_err_2b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL, c int GENERATED ALWAYS AS (b * 3) VIRTUAL);
 -- a whole-row var is a self-reference on steroids, so disallow that too
 CREATE TABLE gtest_err_2c (a int PRIMARY KEY,
-    b int GENERATED ALWAYS AS (num_nulls(gtest_err_2c)) STORED);
+    b int GENERATED ALWAYS AS (num_nulls(gtest_err_2c)) VIRTUAL);
 
 -- invalid reference
-CREATE TABLE gtest_err_3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) STORED);
+CREATE TABLE gtest_err_3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) VIRTUAL);
 
 -- generation expression must be immutable
-CREATE TABLE gtest_err_4 (a int PRIMARY KEY, b double precision GENERATED ALWAYS AS (random()) STORED);
+CREATE TABLE gtest_err_4 (a int PRIMARY KEY, b double precision GENERATED ALWAYS AS (random()) VIRTUAL);
 -- ... but be sure that the immutability test is accurate
-CREATE TABLE gtest2 (a int, b text GENERATED ALWAYS AS (a || ' sec') STORED);
+CREATE TABLE gtest2 (a int, b text GENERATED ALWAYS AS (a || ' sec') VIRTUAL);
 DROP TABLE gtest2;
 
 -- 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);
+CREATE TABLE gtest_err_5a (a int PRIMARY KEY, b int DEFAULT 5 GENERATED ALWAYS AS (a * 2) VIRTUAL);
+CREATE TABLE gtest_err_5b (a int PRIMARY KEY, b int GENERATED ALWAYS AS identity GENERATED ALWAYS AS (a * 2) VIRTUAL);
 
 -- reference to system column not allowed in generated column
 -- (except tableoid, which we test below)
-CREATE TABLE gtest_err_6a (a int PRIMARY KEY, b bool GENERATED ALWAYS AS (xmin <> 37) STORED);
+CREATE TABLE gtest_err_6a (a int PRIMARY KEY, b bool GENERATED ALWAYS AS (xmin <> 37) VIRTUAL);
 
 -- 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);
+CREATE TABLE gtest_err_7a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (avg(a)) VIRTUAL);
+CREATE TABLE gtest_err_7b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (row_number() OVER (ORDER BY a)) VIRTUAL);
+CREATE TABLE gtest_err_7c (a int PRIMARY KEY, b int GENERATED ALWAYS AS ((SELECT a)) VIRTUAL);
+CREATE TABLE gtest_err_7d (a int PRIMARY KEY, b int GENERATED ALWAYS AS (generate_series(1, a)) VIRTUAL);
 
 -- GENERATED BY DEFAULT not allowed
-CREATE TABLE gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT AS (a * 2) STORED);
+CREATE TABLE gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT AS (a * 2) VIRTUAL);
 
 INSERT INTO gtest1 VALUES (1);
 INSERT INTO gtest1 VALUES (2, DEFAULT);  -- ok
@@ -70,7 +70,7 @@ CREATE TABLE gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT AS (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
+-- test that overflow error happens on read
 INSERT INTO gtest1 VALUES (2000000000);
 SELECT * FROM gtest1;
 DELETE FROM gtest1 WHERE a = 2000000000;
@@ -93,8 +93,8 @@ CREATE TABLE gtestm (
   id int PRIMARY KEY,
   f1 int,
   f2 int,
-  f3 int GENERATED ALWAYS AS (f1 * 2) STORED,
-  f4 int GENERATED ALWAYS AS (f2 * 2) STORED
+  f3 int GENERATED ALWAYS AS (f1 * 2) VIRTUAL,
+  f4 int GENERATED ALWAYS AS (f2 * 2) VIRTUAL
 );
 INSERT INTO gtestm VALUES (1, 5, 100);
 MERGE INTO gtestm t USING (VALUES (1, 10), (2, 20)) v(id, f1) ON t.id = v.id
@@ -103,6 +103,14 @@ CREATE TABLE gtestm (
 SELECT * FROM gtestm ORDER BY id;
 DROP TABLE gtestm;
 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
+  b int GENERATED ALWAYS AS (a * 2) VIRTUAL
+);
+INSERT INTO gtestm (a) SELECT g FROM generate_series(1, 10) g;
+MERGE INTO gtestm t USING gtestm AS s ON 2 * t.a = s.b WHEN MATCHED THEN DELETE RETURNING *;
+DROP TABLE gtestm;
+
 -- views
 CREATE VIEW gtest1v AS SELECT * FROM gtest1;
 SELECT * FROM gtest1v;
@@ -134,22 +142,26 @@ CREATE TABLE gtest1_1 () INHERITS (gtest1);
 
 -- can't have generated column that is a child of normal column
 CREATE TABLE gtest_normal (a int, b int);
-CREATE TABLE gtest_normal_child (a int, b int GENERATED ALWAYS AS (a * 2) STORED) INHERITS (gtest_normal);  -- error
-CREATE TABLE gtest_normal_child (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE gtest_normal_child (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL) INHERITS (gtest_normal);  -- error
+CREATE TABLE gtest_normal_child (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 ALTER TABLE gtest_normal_child INHERIT gtest_normal;  -- error
 DROP TABLE gtest_normal, gtest_normal_child;
 
 -- test inheritance mismatches between parent and child
 CREATE TABLE gtestx (x int, b int DEFAULT 10) INHERITS (gtest1);  -- error
 CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS IDENTITY) INHERITS (gtest1);  -- error
-CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) STORED) INHERITS (gtest1);  -- ok, overrides parent
+CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) STORED) INHERITS (gtest1);  -- error
+CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) VIRTUAL) INHERITS (gtest1);  -- ok, overrides parent
 \d+ gtestx
+INSERT INTO gtestx (a, x) VALUES (11, 22);
+SELECT * FROM gtest1;
+SELECT * FROM gtestx;
 
 CREATE TABLE gtestxx_1 (a int NOT NULL, b int);
 ALTER TABLE gtestxx_1 INHERIT gtest1;  -- error
-CREATE TABLE gtestxx_3 (a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE gtestxx_3 (a int NOT NULL, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 ALTER TABLE gtestxx_3 INHERIT gtest1;  -- ok
-CREATE TABLE gtestxx_4 (b int GENERATED ALWAYS AS (a * 2) STORED, a int NOT NULL);
+CREATE TABLE gtestxx_4 (b int GENERATED ALWAYS AS (a * 2) VIRTUAL, a int NOT NULL);
 ALTER TABLE gtestxx_4 INHERIT gtest1;  -- ok
 
 -- test multiple inheritance mismatches
@@ -161,28 +173,28 @@ CREATE TABLE gtesty (x int, b int);
 CREATE TABLE gtest1_y () INHERITS (gtest1, gtesty);  -- error
 DROP TABLE gtesty;
 
-CREATE TABLE gtesty (x int, b int GENERATED ALWAYS AS (x * 22) STORED);
+CREATE TABLE gtesty (x int, b int GENERATED ALWAYS AS (x * 22) VIRTUAL);
 CREATE TABLE gtest1_y () INHERITS (gtest1, gtesty);  -- error
-CREATE TABLE gtest1_y (b int GENERATED ALWAYS AS (x + 1) STORED) INHERITS (gtest1, gtesty);  -- ok
+CREATE TABLE gtest1_y (b int GENERATED ALWAYS AS (x + 1) VIRTUAL) INHERITS (gtest1, gtesty);  -- ok
 \d gtest1_y
 
 -- test correct handling of GENERATED column that's only in child
 CREATE TABLE gtestp (f1 int);
-CREATE TABLE gtestc (f2 int GENERATED ALWAYS AS (f1+1) STORED) INHERITS(gtestp);
+CREATE TABLE gtestc (f2 int GENERATED ALWAYS AS (f1+1) VIRTUAL) INHERITS(gtestp);
 INSERT INTO gtestc values(42);
 TABLE gtestc;
 UPDATE gtestp SET f1 = f1 * 10;
 TABLE gtestc;
 DROP TABLE gtestp CASCADE;
 
--- test stored update
-CREATE TABLE gtest3 (a int, b int GENERATED ALWAYS AS (a * 3) STORED);
+-- test update XXX
+CREATE TABLE gtest3 (a int, b int GENERATED ALWAYS AS (a * 3) VIRTUAL);
 INSERT INTO gtest3 (a) VALUES (1), (2), (3), (NULL);
 SELECT * FROM gtest3 ORDER BY a;
 UPDATE gtest3 SET a = 22 WHERE a = 2;
 SELECT * FROM gtest3 ORDER BY a;
 
-CREATE TABLE gtest3a (a text, b text GENERATED ALWAYS AS (a || '+' || a) STORED);
+CREATE TABLE gtest3a (a text, b text GENERATED ALWAYS AS (a || '+' || a) VIRTUAL);
 INSERT INTO gtest3a (a) VALUES ('a'), ('b'), ('c'), (NULL);
 SELECT * FROM gtest3a ORDER BY a;
 UPDATE gtest3a SET a = 'bb' WHERE a = 'b';
@@ -222,12 +234,12 @@ CREATE TABLE gtest3a (a text, b text GENERATED ALWAYS AS (a || '+' || a) STORED)
 SELECT * FROM gtest3 ORDER BY a;
 
 -- null values
-CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED);
+CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) VIRTUAL);
 INSERT INTO gtest2 VALUES (1);
 SELECT * FROM gtest2;
 
 -- simple column reference for varlena types
-CREATE TABLE gtest_varlena (a varchar, b varchar GENERATED ALWAYS AS (a) STORED);
+CREATE TABLE gtest_varlena (a varchar, b varchar GENERATED ALWAYS AS (a) VIRTUAL);
 INSERT INTO gtest_varlena (a) VALUES('01234567890123456789');
 INSERT INTO gtest_varlena (a) VALUES(NULL);
 SELECT * FROM gtest_varlena ORDER BY a;
@@ -237,7 +249,7 @@ CREATE TABLE gtest_varlena (a varchar, b varchar GENERATED ALWAYS AS (a) STORED)
 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
+    b double_int GENERATED ALWAYS AS ((a * 2, a * 3)) VIRTUAL
 );
 INSERT INTO gtest4 VALUES (1), (6);
 SELECT * FROM gtest4;
@@ -248,168 +260,172 @@ CREATE TABLE gtest4 (
 -- using tableoid is allowed
 CREATE TABLE gtest_tableoid (
   a int PRIMARY KEY,
-  b bool GENERATED ALWAYS AS (tableoid = 'gtest_tableoid'::regclass) STORED
+  b bool GENERATED ALWAYS AS (tableoid = 'gtest_tableoid'::regclass) VIRTUAL
 );
 INSERT INTO gtest_tableoid VALUES (1), (2);
 ALTER TABLE gtest_tableoid ADD COLUMN
-  c regclass GENERATED ALWAYS AS (tableoid) STORED;
+  c regclass GENERATED ALWAYS AS (tableoid) VIRTUAL;
 SELECT * FROM gtest_tableoid;
 
 -- drop column behavior
-CREATE TABLE gtest10 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) STORED);
+CREATE TABLE gtest10 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) VIRTUAL);
 ALTER TABLE gtest10 DROP COLUMN b;  -- fails
 ALTER TABLE gtest10 DROP COLUMN b CASCADE;  -- drops c too
 
 \d gtest10
 
-CREATE TABLE gtest10a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE gtest10a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 ALTER TABLE gtest10a DROP COLUMN b;
 INSERT INTO gtest10a (a) VALUES (1);
 
 -- privileges
 CREATE USER regress_user11;
 
-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 TABLE gtest11v (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) VIRTUAL);
+INSERT INTO gtest11v VALUES (1, 10), (2, 20);
+GRANT SELECT (a, c) ON gtest11v 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);
+CREATE TABLE gtest12v (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) VIRTUAL);
+INSERT INTO gtest12v VALUES (1, 10), (2, 20);
 GRANT SELECT (a, c) ON gtest12s TO regress_user11;
 
 SET ROLE regress_user11;
-SELECT a, b FROM gtest11s;  -- not allowed
-SELECT a, c FROM gtest11s;  -- allowed
+SELECT a, b FROM gtest11v;  -- not allowed
+SELECT a, c FROM gtest11v;  -- allowed
 SELECT gf1(10);  -- not allowed
-SELECT a, c FROM gtest12s;  -- allowed
+SELECT a, c FROM gtest12v;  -- not allowed; TODO: ought to be allowed
 RESET ROLE;
 
 DROP FUNCTION gf1(int);  -- fail
-DROP TABLE gtest11s, gtest12s;
+DROP TABLE gtest11v, gtest12v;
 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));
+CREATE TABLE gtest20 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL CHECK (b < 50));
 INSERT INTO gtest20 (a) VALUES (10);  -- ok
 INSERT INTO gtest20 (a) VALUES (30);  -- violates constraint
 
 ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);  -- violates constraint
 ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);  -- ok
 
-CREATE TABLE gtest20a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE gtest20a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 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);
+CREATE TABLE gtest20b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 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
 
--- 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
+-- not-null constraints (currently not supported)
+CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL 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);
+CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
 ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL;
-INSERT INTO gtest21b (a) VALUES (1);  -- ok
-INSERT INTO gtest21b (a) VALUES (0);  -- violates constraint
+--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
+--INSERT INTO gtest21b (a) VALUES (0);  -- ok now
 
 -- 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);
+CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) VIRTUAL 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) VIRTUAL, PRIMARY KEY (a, b));
+--INSERT INTO gtest22b VALUES (2);
+--INSERT INTO gtest22b VALUES (2);
 
 -- 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;
-
-ALTER TABLE gtest22c ALTER COLUMN b SET EXPRESSION AS (a * 4);
-ANALYZE gtest22c;
-EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 8;
-SELECT * FROM gtest22c WHERE b = 8;
-EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 12;
-SELECT * FROM gtest22c WHERE b * 3 = 12;
-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;
+CREATE TABLE gtest22c (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
+--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;
+
+--ALTER TABLE gtest22c ALTER COLUMN b SET EXPRESSION AS (a * 4);
+--ANALYZE gtest22c;
+--EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 8;
+--SELECT * FROM gtest22c WHERE b = 8;
+--EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 12;
+--SELECT * FROM gtest22c WHERE b * 3 = 12;
+--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;
 
 -- foreign keys
 CREATE TABLE gtest23a (x int PRIMARY KEY, y int);
-INSERT INTO gtest23a VALUES (1, 11), (2, 22), (3, 33);
+--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 gtest23x (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL REFERENCES gtest23a (x) ON UPDATE CASCADE);  -- error
+CREATE TABLE gtest23x (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL 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
+CREATE TABLE gtest23b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL REFERENCES gtest23a (x));
+--\d gtest23b
 
-INSERT INTO gtest23b VALUES (1);  -- ok
-INSERT INTO gtest23b VALUES (5);  -- error
-ALTER TABLE gtest23b ALTER COLUMN b SET EXPRESSION AS (a * 5); -- error
-ALTER TABLE gtest23b ALTER COLUMN b SET EXPRESSION AS (a * 1); -- ok
+--INSERT INTO gtest23b VALUES (1);  -- ok
+--INSERT INTO gtest23b VALUES (5);  -- error
+--ALTER TABLE gtest23b ALTER COLUMN b SET EXPRESSION AS (a * 5); -- error
+--ALTER TABLE gtest23b ALTER COLUMN b SET EXPRESSION AS (a * 1); -- ok
 
-DROP TABLE gtest23b;
-DROP TABLE gtest23a;
+--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 gtest23p (x int, y int GENERATED ALWAYS AS (x * 2) VIRTUAL, 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
+--INSERT INTO gtest23q VALUES (1, 2);  -- ok
+--INSERT INTO gtest23q VALUES (2, 5);  -- error
 
 -- 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
+CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a * 2) VIRTUAL);
+--INSERT INTO gtest24 (a) VALUES (4);  -- ok
+--INSERT INTO gtest24 (a) VALUES (6);  -- error
+CREATE TYPE gtestdomain1range AS range (subtype = gtestdomain1);
+CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) VIRTUAL);
+--INSERT INTO gtest24r (a) VALUES (4);  -- ok
+--INSERT INTO gtest24r (a) VALUES (6);  -- error
 
 -- 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);
+CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) VIRTUAL);
 DROP TYPE gtest_type CASCADE;
 
 -- partitioning cases
 CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint) PARTITION BY RANGE (f1);
 CREATE TABLE gtest_child PARTITION OF gtest_parent (
-    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 2) STORED
+    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 2) VIRTUAL
 ) FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- error
-CREATE TABLE gtest_child (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED);
+CREATE TABLE gtest_child (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- error
 DROP TABLE gtest_parent, gtest_child;
 
-CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f1);
+CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE (f1);
 CREATE TABLE gtest_child PARTITION OF gtest_parent
   FOR VALUES FROM ('2016-07-01') TO ('2016-08-01');  -- inherits gen expr
 CREATE TABLE gtest_child2 PARTITION OF gtest_parent (
-    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 22) STORED  -- overrides gen expr
+    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 22) VIRTUAL  -- overrides gen expr
 ) FOR VALUES FROM ('2016-08-01') TO ('2016-09-01');
 CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
     f3 DEFAULT 42  -- error
@@ -417,6 +433,9 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
 CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
     f3 WITH OPTIONS GENERATED ALWAYS AS IDENTITY  -- error
 ) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
+CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
+    f3 GENERATED ALWAYS AS (f2 * 2) STORED  -- error
+) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
 DROP TABLE gtest_child3;
@@ -426,7 +445,7 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint DEFAULT 42);
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS IDENTITY);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
 DROP TABLE gtest_child3;
-CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) STORED);
+CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) VIRTUAL);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 \d gtest_child
 \d gtest_child2
@@ -457,21 +476,21 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 -- we leave these tables around for purposes of testing dump/reload/upgrade
 
 -- generated columns in partition key (not allowed)
-CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f3);
-CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((f3 * 3));
+CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE (f3);
+CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((f3 * 3));
 
 -- 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 * 2) STORED, ALTER COLUMN b SET EXPRESSION AS (a * 3);
+ALTER TABLE gtest25 ADD COLUMN b int GENERATED ALWAYS AS (a * 2) VIRTUAL, ALTER COLUMN b SET EXPRESSION AS (a * 3);
 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
+ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (b * 4) VIRTUAL;  -- error
+ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (z * 4) VIRTUAL;  -- error
 ALTER TABLE gtest25 ADD COLUMN c int DEFAULT 42,
-  ADD COLUMN x int GENERATED ALWAYS AS (c * 4) STORED;
+  ADD COLUMN x int GENERATED ALWAYS AS (c * 4) VIRTUAL;
 ALTER TABLE gtest25 ADD COLUMN d int DEFAULT 101;
 ALTER TABLE gtest25 ALTER COLUMN d SET DATA TYPE float8,
-  ADD COLUMN y float8 GENERATED ALWAYS AS (d * 4) STORED;
+  ADD COLUMN y float8 GENERATED ALWAYS AS (d * 4) VIRTUAL;
 SELECT * FROM gtest25 ORDER BY a;
 \d gtest25
 
@@ -479,7 +498,7 @@ CREATE TABLE gtest25 (a int PRIMARY KEY);
 CREATE TABLE gtest27 (
     a int,
     b int,
-    x int GENERATED ALWAYS AS ((a + b) * 2) STORED
+    x int GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL
 );
 INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11);
 ALTER TABLE gtest27 ALTER COLUMN a TYPE text;  -- error
@@ -493,7 +512,7 @@ CREATE TABLE gtest27 (
   DROP COLUMN x,
   ALTER COLUMN a TYPE bigint,
   ALTER COLUMN b TYPE bigint,
-  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) STORED;
+  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL;
 \d gtest27
 -- Ideally you could just do this, but not today (and should x change type?):
 ALTER TABLE gtest27
@@ -505,7 +524,7 @@ CREATE TABLE gtest27 (
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -532,7 +551,7 @@ CREATE TABLE gtest29 (
 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE gtest30 ALTER COLUMN b DROP EXPRESSION;
@@ -541,7 +560,7 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 DROP TABLE gtest30 CASCADE;
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE ONLY gtest30 ALTER COLUMN b DROP EXPRESSION;  -- error
@@ -552,7 +571,7 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 -- triggers
 CREATE TABLE gtest26 (
     a int PRIMARY KEY,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 
 CREATE FUNCTION gtest_trigger_func() RETURNS trigger
@@ -614,6 +633,9 @@ CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26
 DROP TRIGGER gtest2 ON gtest26;
 DROP TRIGGER gtest3 ON gtest26;
 
+-- check disallowed modification of virtual columns
+-- TODO
+
 -- Check that an UPDATE of "a" fires the trigger for UPDATE OF b, per
 -- SQL standard.
 CREATE FUNCTION gtest_trigger_func3() RETURNS trigger
@@ -667,7 +689,7 @@ CREATE TABLE gtest28a (
   a int,
   b int,
   c int,
-  x int GENERATED ALWAYS AS (b * 2) STORED
+  x int GENERATED ALWAYS AS (b * 2) VIRTUAL
 );
 
 ALTER TABLE gtest28a DROP COLUMN a;
@@ -675,3 +697,47 @@ CREATE TABLE gtest28a (
 CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
 
 \d gtest28*
+
+
+-- TODO: extra tests to weave into the right places
+
+-- sublinks
+CREATE TABLE t2 (
+    a int,
+    b int GENERATED ALWAYS AS (a * 2),
+    d int DEFAULT 22
+);
+INSERT INTO t2 (a) SELECT g FROM generate_series(1, 10) g;
+SELECT a, b, d, (SELECT t2.b) FROM t2;
+DROP TABLE t2;
+
+-- collate
+/*
+CREATE COLLATION case_insensitive (provider = icu, locale = 'und-u-ks-level2', deterministic = false);
+CREATE TABLE t5 (
+    a text collate "C",
+    b text collate "C" GENERATED ALWAYS AS (a collate case_insensitive) ,
+    d int DEFAULT 22
+);
+INSERT INTO t5 (a, d) values ('d1', 28), ('D2', 27), ('D1', 26);
+SELECT * FROM t5 ORDER BY b ASC, d ASC;
+DROP TABLE t5;
+DROP COLLATION case_insensitive;
+*/
+
+-- composite type dependencies
+CREATE TABLE at_tab1 (a int, b text GENERATED ALWAYS AS ('hello'), c text);
+CREATE TABLE at_tab2 (x int, y at_tab1);
+ALTER TABLE at_tab1 ALTER COLUMN b TYPE varchar;  -- fails
+DROP TABLE at_tab1, at_tab2;
+
+-- Check it for a partitioned table, too
+CREATE TABLE at_tab1 (a int, b text GENERATED ALWAYS AS ('hello'), c text) PARTITION BY LIST (a);
+CREATE TABLE at_tab2 (x int, y at_tab1);
+ALTER TABLE at_tab1 ALTER COLUMN b TYPE varchar;  -- fails
+DROP TABLE at_tab1, at_tab2;
+
+-- attach mixing generated kinds
+CREATE TABLE bar1 (a integer, b integer GENERATED ALWAYS AS (22)) PARTITION BY RANGE (a);
+CREATE TABLE bar2 (a integer, b integer GENERATED ALWAYS AS (22) STORED);
+ALTER TABLE bar1 ATTACH PARTITION bar2 DEFAULT;  -- error
diff --git a/src/test/subscription/t/011_generated.pl b/src/test/subscription/t/011_generated.pl
index 8b2e5f4708e..faee155daa7 100644
--- a/src/test/subscription/t/011_generated.pl
+++ b/src/test/subscription/t/011_generated.pl
@@ -21,11 +21,11 @@
 my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
 
 $node_publisher->safe_psql('postgres',
-	"CREATE TABLE tab1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED)"
+	"CREATE TABLE tab1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (a * 3) VIRTUAL)"
 );
 
 $node_subscriber->safe_psql('postgres',
-	"CREATE TABLE tab1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 22) STORED, c int)"
+	"CREATE TABLE tab1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 22) STORED, c int GENERATED ALWAYS AS (a * 33) VIRTUAL, d int)"
 );
 
 # data for initial sync
@@ -42,10 +42,10 @@
 # Wait for initial sync of all subscriptions
 $node_subscriber->wait_for_subscription_sync;
 
-my $result = $node_subscriber->safe_psql('postgres', "SELECT a, b FROM tab1");
-is( $result, qq(1|22
-2|44
-3|66), 'generated columns initial sync');
+my $result = $node_subscriber->safe_psql('postgres', "SELECT a, b, c FROM tab1");
+is( $result, qq(1|22|33
+2|44|66
+3|66|99), 'generated columns initial sync');
 
 # data to replicate
 
@@ -56,11 +56,11 @@
 $node_publisher->wait_for_catchup('sub1');
 
 $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1");
-is( $result, qq(1|22|
-2|44|
-3|66|
-4|88|
-6|132|), 'generated columns replicated');
+is( $result, qq(1|22|33|
+2|44|66|
+3|66|99|
+4|88|132|
+6|132|198|), 'generated columns replicated');
 
 # try it with a subscriber-side trigger
 
@@ -69,7 +69,7 @@
 CREATE FUNCTION tab1_trigger_func() RETURNS trigger
 LANGUAGE plpgsql AS $$
 BEGIN
-  NEW.c := NEW.a + 10;
+  NEW.d := NEW.a + 10;
   RETURN NEW;
 END $$;
 
@@ -88,12 +88,12 @@ BEGIN
 
 $result =
   $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY 1");
-is( $result, qq(1|22|
-2|44|
-3|66|
-4|88|
-6|132|
-8|176|18
-9|198|19), 'generated columns replicated with trigger');
+is( $result, qq(1|22|33|
+2|44|66|
+3|66|99|
+4|88|132|
+6|132|198|
+8|176|264|18
+9|198|297|19), 'generated columns replicated with trigger');
 
 done_testing();

base-commit: 894be11adfa60ad1ce5f74534cf5f04e66d51c30
-- 
2.46.0

---

From: jian he <[email-masked]>
Date: Thu, 29 Aug 2024 21:35:49 +0800

> you have the wrong column name in the check constraint.  I'm not sure if

That's my mistake. sorry for the noise.

> issues you presented.  As I mentioned, I'll look into improving the

based on your latest patch (v4-0001-Virtual-generated-columns.patch),
I did some minor cosmetic code change
and tried to address get_attgenerated overhead.

basically in expand_generated_columns_in_query
and expand_generated_columns_in_expr preliminary collect (reloid,attnum)
that have generated_virtual flag into expand_generated_context.
later in expand_generated_columns_mutator use the collected information.

deal with wholerow within the expand_generated_columns_mutator seems
tricky, will try later.

---

From: jian he <[email-masked]>
Date: Mon, 2 Sep 2024 21:25:15 +0800

> descending into subqueries as needed.

CREATE TABLE gtest1 (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
INSERT INTO gtest1 VALUES (1,default), (2, DEFAULT);

select b from  (SELECT b FROM gtest1) sub;
here we only need to translate the second "b" to (a *2), not the first one.
but these two "b" query tree representation almost the same (varno,
varattno, varlevelsup)

I am not sure how ReplaceVarsFromTargetList can disambiguate this?
Currently v4-0001-Virtual-generated-columns.patch
works. because v4 properly tags the main query hasGeneratedVirtual to false,
and tag subquery's hasGeneratedVirtual to true.

---

From: Nazir Bilal Yavuz <[email-masked]>
Date: Mon, 2 Sep 2024 16:29:11 +0300

Hi,

> issues you presented.  As I mentioned, I'll look into improving the

xid_wraparound test started to fail after edee0c621d. It seems the
error message used in xid_wraparound/002_limits is updated. The patch
that applies the same update to the test file is attached.

-- 
Regards,
Nazir Bilal Yavuz
Microsoft
From 748721898e8171d35d54ffe2b6edb38b9f5b020d Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <[email-masked]>
Date: Mon, 2 Sep 2024 16:18:57 +0300
Subject: [PATCH v1] Fix xid_wraparound/002_limits test

Error message used in xid_wraparound/002_limits is updated in edee0c621d.
Apply the same update in the test file as well.
---
 src/test/modules/xid_wraparound/t/002_limits.pl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/modules/xid_wraparound/t/002_limits.pl b/src/test/modules/xid_wraparound/t/002_limits.pl
index aca3fa15149..889689d3bde 100644
--- a/src/test/modules/xid_wraparound/t/002_limits.pl
+++ b/src/test/modules/xid_wraparound/t/002_limits.pl
@@ -103,7 +103,7 @@ $ret = $node->psql(
 	stderr => \$stderr);
 like(
 	$stderr,
-	qr/ERROR:  database is not accepting commands that assign new XIDs to avoid wraparound data loss in database "postgres"/,
+	qr/ERROR:  database is not accepting commands that assign new transaction IDs to avoid wraparound data loss in database "postgres"/,
 	"stop-limit");
 
 # Finish the old transaction, to allow vacuum freezing to advance
-- 
2.45.2

---

From: jian he <[email-masked]>
Date: Tue, 3 Sep 2024 12:59:37 +0800

> deal with wholerow within the expand_generated_columns_mutator seems

please just ignore v4-0001-Virtual-generated-columns_minorchange.no-cfbot,
which I made some mistakes, but the tests still passed.

please checking this mail attached
v5-0001-Virtual-generated-wholerow-var-and-virtual-che.no-cfbot

It solves:
1. minor cosmetic changes.
2. virtual generated column wholerow var reference, tests added.
3. optimize get_attgenerated overhead, instead of for each var call
get_attgenerated.
  walk through the query tree, collect the virtual column's relation
oid, and the virtual generated column's attnum
and use this information later.

I will check the view insert case later.

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 4 Sep 2024 10:40:06 +0200

On 21.08.24 12:51, Dean Rasheed wrote:
> On Wed, 21 Aug 2024 at 08:00, Peter Eisentraut<[email-masked]>  wrote:

Here is an implementation of this.  It's much nicer!  It also appears to 
fix all the additional test cases that have been presented.  (I haven't 
integrated them into the patch set yet.)

I left the 0001 patch alone for now and put the new rewriting 
implementation into 0002.  (Unfortunately, the diff is kind of useless 
for visual inspection.)  Let me know if this matches what you had in 
mind, please.  Also, is this the right place in fireRIRrules()?
From 9eabb5d196f63edd934bb9b7be0c246abbea260a Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Thu, 29 Aug 2024 11:49:48 +0200
Subject: [PATCH v6 1/2] Virtual generated columns

TODO:

 
 drop table test8;

 
 
 select * from grem1;
- a  | b  
-----+----
-  1 |  2
- 22 | 44
+ a  | b  | c  
+----+----+----
+  1 |  2 |  3
+ 22 | 44 | 66
 (2 rows)
 
 delete from grem1;
 -- test copy from
 copy grem1 from stdin;
 select * from gloc1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 |  
+ 2 | 4 |  
 (2 rows)
 
 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;

 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;

   <para>
 
   <para>

+     <para>

 
    <varlistentry>
 
      <para>
 
      <para>

      <para>
 
      <para>
 
    <para>

    <para>

 
 		if (att->attisdropped)
 			continue;

 
 	/*

 

 			Assert(expr != NULL);

 			k = -1;

 			/*
index b3cc6f8f690..0914e0566b0 100644

 

 
+	/*
 
 	/*

 

 			/*
 
 				/*

 /*

 			/*
 
 			/*
 
index a6c47f61e0d..0baad880431 100644

 

 

 

 

 

 

 

 
 
 					/*
 
+opt_virtual_or_stored:

 

 
 
 	/*
index 00e7024563e..cb2e864ad45 100644

index 6d59a2bb8dc..f2713eaef23 100644

 /*

+/*

 
 
index 63efc55f09e..dca8a5a4a21 100644
 
index dacb033e989..29662c5c19a 100644
@@ -15994,6 +15994,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)

 /*

 

 

 

 

 

 

 

 

 

 
+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,

 
 \d gtest1
 
 -- duplicate generated

+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
 
 \d gtest1_1

 

 
 
 -- null values

 -- drop column behavior

 
 \d gtest_child2
 
 \d gtest_child3

 
 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3

 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3

 
 
 \d gtest25

 SELECT * FROM gtest27;
  a | b  | x  

 SELECT * FROM gtest27;
  a | b  | x  
 
 \d gtest29

 \d gtest29

 \d gtest29

 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
 
 \d gtest30_1

 \d gtest30_1

 

 

 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,

 

 
 \d gtest1
 
 -- duplicate generated

 -- invalid reference

 

 

+CREATE TABLE gtestm (
+  a int PRIMARY KEY,

 

 
 
--- test stored update

 -- null values

 -- drop column behavior
 
 \d gtest10

 -- privileges

 
 
 SET ROLE regress_user11;

 -- check constraints

 
 
--- not-null constraints

 -- index constraints
 
 -- indexes
 
 -- foreign keys

 
 
-DROP TABLE gtest23b;
-DROP TABLE gtest23a;
+--DROP TABLE gtest23b;
+--DROP TABLE gtest23a;

 
 -- domains

 -- partitioning cases

 

 

 \d gtest28*
+
+

 
 # data for initial sync
@@ -42,10 +42,10 @@

 # data to replicate
 
@@ -56,11 +56,11 @@

 
@@ -69,7 +69,7 @@

 $result =
 
 done_testing();

base-commit: a68159ff2b32f290b1136e2940470d50b8491301
-- 
2.46.0

From 7bfe4fc9ce81124d9e7fdc24a1f46099bcaec726 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Wed, 4 Sep 2024 09:38:42 +0200
Subject: [PATCH v6 2/2] Use ReplaceVarsFromTargetList() for rewriting virtual
 columns

---
 src/backend/parser/analyze.c         |   8 --
 src/backend/parser/parse_clause.c    |   4 -
 src/backend/parser/parse_merge.c     |   1 -
 src/backend/parser/parse_relation.c  |   3 -
 src/backend/rewrite/rewriteHandler.c | 185 ++++++++-------------------
 src/include/nodes/parsenodes.h       |   2 -
 src/include/parser/parse_node.h      |   1 -
 7 files changed, 54 insertions(+), 150 deletions(-)

diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index e3936a0b8ea..e901203424d 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -568,7 +568,6 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
-	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;

@@ -995,7 +994,6 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
-	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;

@@ -1461,7 +1459,6 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
-	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;
 
 	foreach(l, stmt->lockingClause)
 	{
@@ -1688,7 +1685,6 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	qry->jointree = makeFromExpr(pstate->p_joinlist, NULL);
 
 	qry->hasSubLinks = pstate->p_hasSubLinks;
-	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;

@@ -1940,7 +1936,6 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
-	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;
 
 	foreach(l, lockingClause)
 	{
@@ -2415,7 +2410,6 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
-	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;

@@ -2483,7 +2477,6 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
-	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;

@@ -2850,7 +2843,6 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
 	qry->hasWindowFuncs = pstate->p_hasWindowFuncs;
 	qry->hasTargetSRFs = pstate->p_hasTargetSRFs;
 	qry->hasAggs = pstate->p_hasAggs;
-	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;
 
 	foreach(l, sstmt->lockingClause)
 	{
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 48524ac3fc2..8118036495b 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -207,10 +207,6 @@ setTargetTable(ParseState *pstate, RangeVar *relation,
 	pstate->p_target_relation = parserOpenTable(pstate, relation,
 												RowExclusiveLock);
 
-	if (pstate->p_target_relation->rd_att->constr &&
-		pstate->p_target_relation->rd_att->constr->has_generated_virtual)
-		pstate->p_hasGeneratedVirtual = true;
-
 	/*
 	 * Now build an RTE and a ParseNamespaceItem.
 	 */
diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c
index 350e9e18885..87df79027d7 100644
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -405,7 +405,6 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
 
 	qry->hasTargetSRFs = false;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
-	qry->hasGeneratedVirtual = pstate->p_hasGeneratedVirtual;

diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index b864749f09c..7a3199ae1d3 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -1515,9 +1515,6 @@ addRangeTableEntry(ParseState *pstate,
 	rte->eref = makeAlias(refname, NIL);
 	buildRelationAliases(rel->rd_att, alias, rte->eref);
 
-	if (rel->rd_att->constr && rel->rd_att->constr->has_generated_virtual)
-		pstate->p_hasGeneratedVirtual = true;
-
 	/*
 	 * Set flags and initialize access permissions.
 	 *
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index f2713eaef23..0241d471571 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -44,7 +44,6 @@
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
-#include "utils/syscache.h"

 /* We use a list of these to detect recursion in RewriteQuery */
@@ -91,8 +90,7 @@ static List *matchLocks(CmdType event, Relation relation,
 						int varno, Query *parsetree, bool *hasUpdate);
 static Query *fireRIRrules(Query *parsetree, List *activeRIRs);
 static Bitmapset *adjust_view_column_set(Bitmapset *cols, List *targetlist);
-struct expand_generated_context;
-static Query *expand_generated_columns_in_query(Query *query, struct expand_generated_context *context);
+static Node *expand_generated_columns_internal(Node *node, Relation rel, int rt_index, RangeTblEntry *rte);

 /*
@@ -2139,6 +2137,9 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 			}
 		}
 
+		/* Expand virtual generated columns of this table */
+		parsetree = (Query *) expand_generated_columns_internal((Node *) parsetree, rel, rt_index, rte);
+
 		table_close(rel, NoLock);
 	}
 
@@ -4370,84 +4371,66 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)

 /*
- * Virtual generated columns support
+ * Expand virtual generated columns
+ *
+ * If the table contains virtual generated columns, build a target list
+ * containing the expanded expressions and use ReplaceVarsFromTargetList() to
+ * do the replacements.
  */
-
-struct expand_generated_context
-{
-	/* list of range tables, innermost last */
-	List	   *rtables;
-
-	/* incremented for every level where it's true */
-	int			ancestor_has_virtual;
-};
-
 static Node *
-expand_generated_columns_mutator(Node *node, struct expand_generated_context *context)
+expand_generated_columns_internal(Node *node, Relation rel, int rt_index, RangeTblEntry *rte)
 {
-	if (node == NULL)
-		return NULL;
+	TupleDesc	tupdesc;
 
-	if (IsA(node, Var))
+	tupdesc = RelationGetDescr(rel);
+	if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
 	{
-		Var		   *v = (Var *) node;
-		Oid			relid;
-		AttrNumber	attnum;
-		List	   *rtable = list_nth_node(List,
-										   context->rtables,
-										   list_length(context->rtables) - v->varlevelsup - 1);
-
-		relid = rt_fetch(v->varno, rtable)->relid;
-		attnum = v->varattno;
+		List	   *tlist = NIL;
 
-		if (!relid || !attnum)
-			return node;
-
-		if (get_attgenerated(relid, attnum) == ATTRIBUTE_GENERATED_VIRTUAL)
+		for (int i = 0; i < tupdesc->natts; i++)
 		{
-			Relation	rt_entry_relation = table_open(relid, NoLock);
-			Oid			attcollid;
-
-			node = build_column_default(rt_entry_relation, attnum);
-			if (node == NULL)
-				elog(ERROR, "no generation expression found for column number %d of table \"%s\"",
-					 attnum, RelationGetRelationName(rt_entry_relation));
+			Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
 
-			/*
-			 * If the column definition has a collation and it is different
-			 * from the collation of the generation expression, put a COLLATE
-			 * clause around the expression.
-			 */
-			attcollid = GetSysCacheOid(ATTNUM, Anum_pg_attribute_attcollation, relid, attnum, 0, 0);
-			if (attcollid && attcollid != exprCollation(node))
+			if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
 			{
-				CollateExpr *ce = makeNode(CollateExpr);
+				Node	   *defexpr;
+				int			attnum = i + 1;
+				Oid			attcollid;
+				TargetEntry *te;
 
-				ce->arg = (Expr *) node;
-				ce->collOid = attcollid;
-				ce->location = -1;
+				defexpr = build_column_default(rel, attnum);
+				if (defexpr == NULL)
+					elog(ERROR, "no generation expression found for column number %d of table \"%s\"",
+						 attnum, RelationGetRelationName(rel));
 
-				node = (Node *) ce;
-			}
+				/*
+				 * If the column definition has a collation and it is
+				 * different from the collation of the generation expression,
+				 * put a COLLATE clause around the expression.
+				 */
+				attcollid = attr->attcollation;
+				if (attcollid && attcollid != exprCollation(defexpr))
+				{
+					CollateExpr *ce = makeNode(CollateExpr);
 
-			IncrementVarSublevelsUp(node, v->varlevelsup, 0);
-			ChangeVarNodes(node, 1, v->varno, v->varlevelsup);
+					ce->arg = (Expr *) defexpr;
+					ce->collOid = attcollid;
+					ce->location = -1;
 
-			table_close(rt_entry_relation, NoLock);
-		}
+					defexpr = (Node *) ce;
+				}
 
-		return node;
-	}
-	else if (IsA(node, Query))
-	{
-		Query	   *query = (Query *) node;
+				ChangeVarNodes(defexpr, 1, rt_index, 0);
 
-		query = expand_generated_columns_in_query(query, context);
+				te = makeTargetEntry((Expr *) defexpr, attnum, 0, false);
+				tlist = lappend(tlist, te);
+			}
+		}
 
-		return (Node *) query;
+		node = ReplaceVarsFromTargetList(node, rt_index, 0, rte, tlist, REPLACEVARS_CHANGE_VARNO, rt_index, NULL);
 	}
-	else
-		return expression_tree_mutator(node, expand_generated_columns_mutator, context);
+
+	return node;
 }
 
 Node *
@@ -4458,76 +4441,19 @@ expand_generated_columns_in_expr(Node *node, Relation rel)
 	if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
 	{
 		RangeTblEntry *rte;
-		List	   *rtable;
-		struct expand_generated_context context;
 
-		/*
-		 * Make a dummy range table for a single relation.  For the benefit of
-		 * triggers, add the same entry twice, so it covers PRS2_OLD_VARNO and
-		 * PRS2_NEW_VARNO.
-		 */
 		rte = makeNode(RangeTblEntry);
 		rte->relid = RelationGetRelid(rel);
-		rtable = list_make2(rte, rte);
-		context.rtables = list_make1(rtable);
-
-		return expression_tree_mutator(node, expand_generated_columns_mutator, &context);
-	}
-	else
-		return node;
-}
-
-/*
- * Expand virtual generated columns in a Query.  We do some optimizations here
- * to avoid digging through the whole Query unless necessary.
- */
-static Query *
-expand_generated_columns_in_query(Query *query, struct expand_generated_context *context)
-{
-	context->rtables = lappend(context->rtables, query->rtable);
-	if (query->hasGeneratedVirtual)
-		context->ancestor_has_virtual++;
-
-	/*
-	 * If any table in the query has a virtual column or there is a sublink,
-	 * then we need to do the whole walk.
-	 */
-	if (query->hasGeneratedVirtual || query->hasSubLinks || context->ancestor_has_virtual)
-	{
-		query = query_tree_mutator(query,
-								   expand_generated_columns_mutator,
-								   context,
-								   QTW_DONT_COPY_QUERY);
-	}
-
-	/*
-	 * Else we only need to process subqueries.
-	 */
-	else
-	{
-		ListCell   *lc;
-
-		foreach(lc, query->rtable)
-		{
-			RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
-
-			if (rte->rtekind == RTE_SUBQUERY)
-				rte->subquery = expand_generated_columns_in_query(rte->subquery, context);
-		}
 
-		foreach(lc, query->cteList)
-		{
-			CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
-
-			cte->ctequery = (Node *) expand_generated_columns_in_query(castNode(Query, cte->ctequery), context);
-		}
+		/*
+		 * XXX For the benefit of triggers, make two passes, so it covers
+		 * PRS2_OLD_VARNO and PRS2_NEW_VARNO.
+		 */
+		node = expand_generated_columns_internal(node, rel, 1, rte);
+		node = expand_generated_columns_internal(node, rel, 2, rte);
 	}
 
-	if (query->hasGeneratedVirtual)
-		context->ancestor_has_virtual--;
-	context->rtables = list_truncate(context->rtables, list_length(context->rtables) - 1);
-
-	return query;
+	return node;
 }

@@ -4575,12 +4501,9 @@ QueryRewrite(Query *parsetree)
 	foreach(l, querylist)
 	{
 		Query	   *query = (Query *) lfirst(l);
-		struct expand_generated_context context = {0};

-		query = expand_generated_columns_in_query(query, &context);
-
 		query->queryId = input_query_id;
 
 		results = lappend(results, query);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 355f52199fa..0e91c084a01 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -160,8 +160,6 @@ typedef struct Query
 	bool		hasForUpdate pg_node_attr(query_jumble_ignore);
 	/* rewriter has applied some RLS policy */
 	bool		hasRowSecurity pg_node_attr(query_jumble_ignore);
-	/* some table has a virtual generated column */
-	bool		hasGeneratedVirtual pg_node_attr(query_jumble_ignore);
 	/* is a RETURN statement */
 	bool		isReturn pg_node_attr(query_jumble_ignore);
 
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 1b02128548b..5b781d87a9d 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -225,7 +225,6 @@ struct ParseState
 	bool		p_hasTargetSRFs;
 	bool		p_hasSubLinks;
 	bool		p_hasModifyingCTE;
-	bool		p_hasGeneratedVirtual;

-- 
2.46.0

---

From: Dean Rasheed <[email-masked]>
Date: Wed, 4 Sep 2024 11:33:53 +0100

On Wed, 4 Sept 2024 at 09:40, Peter Eisentraut <[email-masked]> wrote:
> > corresponding generated expressions.
>

Yes, that's what I had in mind except that it has to be called from
the second loop in fireRIRrules(), after any RLS policies have been
added, because it's possible for a RLS policy expression to refer to
virtual generated columns. It's OK to do it in the same loop that
expands RLS policies, because such policies can only refer to columns
of the same relation, so once the RLS policies have been expanded for
a given relation, nothing else should get added to the query that can
refer to columns of that relation, at that query level, so at that
point it should be safe to expand virtual generated columns.

Regards,
Dean

---

From: jian he <[email-masked]>
Date: Thu, 5 Sep 2024 16:27:00 +0800

> mind, please.  Also, is this the right place in fireRIRrules()?

hi. some minor issues.

in get_dependent_generated_columns we can

            /* skip if not generated column */
            if (!TupleDescAttr(tupdesc, defval->adnum - 1)->attgenerated)
                continue;
change to
            /* skip if not generated stored column */
            if (!(TupleDescAttr(tupdesc, defval->adnum -
1)->attgenerated == ATTRIBUTE_GENERATED_STORED))
                continue;

in ExecInitStoredGenerated
"if ((tupdesc->constr && tupdesc->constr->has_generated_stored)))"
is true.
then later we finish the loop
(for (int i = 0; i < natts; i++) loop)

we can "Assert(ri_NumGeneratedNeeded > 0)"
so we can ensure once has_generated_stored flag is true,
then we should have at least one stored generated attribute.

similarly, in expand_generated_columns_internal
we can aslo add "Assert(list_length(tlist) > 0);"
above
node = ReplaceVarsFromTargetList(node, rt_index, 0, rte, tlist,
REPLACEVARS_CHANGE_VARNO, rt_index, NULL);

@@ -2290,7 +2291,9 @@ ExecBuildSlotValueDescription(Oid reloid,
if (table_perm || column_perm)
{
- if (slot->tts_isnull[i])
+ if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+ val = "virtual";
+ else if (slot->tts_isnull[i])
    val = "null";
else
{
Oid  foutoid;
bool typisvarlena;
getTypeOutputInfo(att->atttypid, &foutoid, &typisvarlena);
val = OidOutputFunctionCall(foutoid, slot->tts_values[i]);
}

we can add Assert here, if i understand it correctly, like
 if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
{
Assert(slot->tts_isnull[i]);
 val = "virtual";
}

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 9 Sep 2024 08:02:54 +0200

> point it should be safe to expand virtual generated columns.

If I move the code like that, then the postgres_fdw test fails.  So 
there is some additional interaction there that I need to study.

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 9 Sep 2024 08:06:40 +0200

> 1)->attgenerated == ATTRIBUTE_GENERATED_STORED))

I need to study more what to do with this function.  I'm not completely 
sure whether this should apply only to stored generated columns.

> then we should have at least one stored generated attribute.

This is technically correct, but this code isn't touched by this patch, 
so I don't think it belongs here.

> REPLACEVARS_CHANGE_VARNO, rt_index, NULL);

Ok, I'll add that.

> Assert(slot->tts_isnull[i]);

Also technically correct, but I don't see what benefit this would bring. 
  The code guarded by that assert would not make use of the thing being 
asserted.

---

From: jian he <[email-masked]>
Date: Mon, 16 Sep 2024 17:22:29 +0800

in v7.

doc/src/sgml/ref/alter_table.sgml
<phrase>and <replaceable
class="parameter">column_constraint</replaceable> is:</phrase>

section need representation of:
GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) [VIRTUAL]

in RelationBuildTupleDesc(Relation relation)
we need to add "constr->has_generated_virtual" for the following code?

    if (constr->has_not_null ||
        constr->has_generated_stored ||
        ndef > 0 ||
        attrmiss ||
        relation->rd_rel->relchecks > 0)

also seems there will be table_rewrite for adding virtual generated
columns, but we can avoid that.
The attached patch is the change and the tests.

i've put the tests in src/test/regress/sql/fast_default.sql,
since it already has event triggers and trigger functions, we don't
want to duplicate it.

---

From: jian he <[email-masked]>
Date: Wed, 18 Sep 2024 10:38:10 +0800

> in v7.
seems I am confused with the version number.

here, I attached another minor change in tests.

make
ERROR:  invalid ON DELETE action for foreign key constraint containing
generated column
becomes
ERROR:  foreign key constraints on virtual generated columns are not supported

change contrib/pageinspect/sql/page.sql
expand information on t_infomask, t_bits information.

change RelationBuildLocalRelation
make the transient TupleDesc->TupleConstr three bool flags more accurate.

---

From: Peter Eisentraut <[email-masked]>
Date: Sun, 29 Sep 2024 22:09:22 -0400

> there is some additional interaction there that I need to study.

This was actually a trivial issue.  The RLS loop skips relation kinds 
that can't have RLS policies, which includes foreign tables.  So I did 
this slightly differently and added another loop below the RLS loop for 
the virtual columns.  Now this all works.

I'm attaching a consolidated patch here, so we have something up to date 
on the record.  I haven't worked through all the other recent feedback 
from Jian He yet; I'll do that next.
From 53499561979cd7af71635194eb86e686643c9f9a Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Sun, 29 Sep 2024 21:55:05 -0400
Subject: [PATCH v7] Virtual generated columns

TODO:

 src/backend/commands/tablecmds.c              | 122 ++-
 src/backend/commands/trigger.c                |  49 +-
 src/backend/parser/parse_relation.c           |   6 +-
 src/backend/rewrite/rewriteHandler.c          | 111 ++-
 src/backend/utils/cache/partcache.c           |   2 +
 src/include/nodes/parsenodes.h                |   1 +
 54 files changed, 1311 insertions(+), 709 deletions(-)

index 3fd3869c82a..bacd371a9d5 100644
 
 drop table test8;
index 346e4ee142c..923687d0630 100644
 
index f2bcd6aa98c..614e8135e0e 100644

 
 select * from grem1;
- a  | b  
-----+----
-  1 |  2
- 22 | 44
+ a  | b  | c  
+----+----+----
+  1 |  2 |  3
+ 22 | 44 | 66
 (2 rows)
 
 delete from grem1;
 -- test copy from
 copy grem1 from stdin;
 select * from gloc1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 |  
+ 2 | 4 |  
 (2 rows)
 
 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;

 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;
index 372fe6dad15..32e16bfc0e2 100644
index bfb97865e18..64d0fc61f62 100644
 
index 8ab0ddb112f..4591be60488 100644
 
   <para>
 
   <para>

+     <para>

 
    <varlistentry>
 
      <para>
 
      <para>
index c1855b8d827..1f7ccbca830 100644

      <para>
 
      <para>
@@ -2447,9 +2451,9 @@ <title>Multiple Identity Columns</title>
    <title>Generated Columns</title>
 
    <para>
 
index 49382d07fa8..dab17f7ae4f 100644
 
    <para>

 
 		if (att->attisdropped)
 			continue;
index 78e59384d1c..287f03853ad 100644
 
index 38fb4c3ef23..713ca5e8661 100644
@@ -1039,6 +1039,10 @@ examine_attribute(Relation onerel, int attnum, Node *index_expr)
 	if (attr->attisdropped)
 		return NULL;
 
index 130cebd6588..ed2377aeed2 100644
@@ -1104,6 +1104,9 @@ DefineIndex(Oid tableId,
@@ -1113,14 +1116,22 @@ DefineIndex(Oid tableId,
 
 	/*
 
@@ -1133,6 +1144,22 @@ DefineIndex(Oid tableId,

 
 
 			Assert(expr != NULL);

 			k = -1;

 			/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7405023e77b..f67d435e5dc 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -2943,6 +2943,15 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 									 errhint("A child table column cannot be generated unless its parent column is.")));
 					}
 
@@ -3214,6 +3223,15 @@ MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const
 
+	if (inhdef->generated && newdef->generated && newdef->generated != inhdef->generated)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+				 errmsg("column \"%s\" inherits from generated column of different kind",
+						inhdef->colname),
+				 errdetail("Parent column is %s, child column is %s.",
+						   inhdef->generated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL",
+						   newdef->generated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL")));
+
 	/*
 	 * If new def has a default, override previous default
 	 */
@@ -6040,7 +6058,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
 		{
 			case CONSTR_CHECK:
 				needscan = true;
-				con->qualstate = ExecPrepareExpr((Expr *) con->qual, estate);
+				con->qualstate = ExecPrepareExpr((Expr *) expand_generated_columns_in_expr(con->qual, newrel ? newrel : oldrel, 1), estate);
 				break;
 			case CONSTR_FOREIGN:
 				/* Nothing to do here */
@@ -7761,6 +7779,14 @@ ATExecSetNotNull(AlteredTableInfo *tab, Relation rel,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
@@ -8358,7 +8384,18 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 
@@ -8515,17 +8552,30 @@ ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMOD
 
@@ -8668,6 +8718,16 @@ ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa
 
+	/*
@@ -9778,6 +9838,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -12924,8 +12997,12 @@ ATPrepAlterColumnType(List **wqueue,
 					   list_make1_oid(rel->rd_rel->reltype),
 					   0);
 
@@ -12998,11 +13075,12 @@ ATPrepAlterColumnType(List **wqueue,
 
@@ -15963,6 +16041,14 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 
@@ -18096,8 +18182,11 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 
 			/*
@@ -18179,9 +18268,12 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 				}
 
 				/*
index 3671e82535e..7602b271256 100644

 /*
@@ -943,6 +946,13 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 
@@ -2503,6 +2513,8 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
 
 			/*
@@ -3060,6 +3072,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
 		}
 		else if (newtuple != oldtuple)
 		{
+			check_modified_virtual_generated(RelationGetDescr(relinfo->ri_RelationDesc), newtuple);
+
 			ExecForceStoreHeapTuple(newtuple, newslot, false);
 
 			/*
@@ -3490,6 +3504,8 @@ TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
 
+			tgqual = (Node *) expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc, PRS2_OLD_VARNO);
+			tgqual = (Node *) expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc, PRS2_NEW_VARNO);
@@ -6600,3 +6616,34 @@ pg_trigger_depth(PG_FUNCTION_ARGS)
index 9fd988cc992..62d453687f2 100644

+		/* Internal error: somebody forgot to expand it. */
+		if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			elog(ERROR, "unexpected virtual generated column reference");
+
 		if (attr->attisdropped)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 728cdee480b..a1c0dcab059 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1761,6 +1761,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
 			Expr	   *checkconstr;
 
 			checkconstr = stringToNode(check[i].ccbin);
+			checkconstr = (Expr *) expand_generated_columns_in_expr((Node *) checkconstr, rel, 1);
 			resultRelInfo->ri_ConstraintExprs[i] =
 				ExecPrepareExpr(checkconstr, estate);
 		}
@@ -2308,7 +2309,9 @@ ExecBuildSlotValueDescription(Oid reloid,
 
index b1d4642c59b..81d7b1161de 100644
@@ -641,7 +641,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -789,7 +789,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

 
@@ -3974,7 +3974,7 @@ ColConstraintElem:
 					n->location = @1;
 					$$ = (Node *) n;
 				}
-			| GENERATED generated_when AS '(' a_expr ')' STORED
+			| GENERATED generated_when AS '(' a_expr ')' opt_virtual_or_stored
 				{
 					Constraint *n = makeNode(Constraint);
 
@@ -3982,6 +3982,7 @@ ColConstraintElem:
 					n->generated_when = $2;
 					n->raw_expr = $5;
 					n->cooked_expr = NULL;
+					n->generated_kind = $7;
 					n->location = @1;
 
 					/*
@@ -4028,6 +4029,12 @@ generated_when:
 			| BY DEFAULT	{ $$ = ATTRIBUTE_IDENTITY_BY_DEFAULT; }
 		;
 
+opt_virtual_or_stored:
+			STORED			{ $$ = ATTRIBUTE_GENERATED_STORED; }
+			| VIRTUAL		{ $$ = ATTRIBUTE_GENERATED_VIRTUAL; }
+			| /*EMPTY*/		{ $$ = ATTRIBUTE_GENERATED_VIRTUAL; }
+		;
+
 /*
  * ConstraintAttr represents constraint attributes, which we parse as if
  * they were independent constraint clauses, in order to avoid shift/reduce
@@ -17845,6 +17852,7 @@ unreserved_keyword:
 			| VERSION_P
 			| VIEW
 			| VIEWS
+			| VIRTUAL
 			| VOLATILE
 			| WHITESPACE_P
 			| WITHIN
@@ -18499,6 +18507,7 @@ bare_label_keyword:
 			| VERSION_P
 			| VIEW
 			| VIEWS
+			| VIRTUAL
 			| VOLATILE
 			| WHEN
 			| WHITESPACE_P
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 8075b1b8a1b..6f8f96b34da 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -704,7 +704,11 @@ scanNSItemForColumn(ParseState *pstate, ParseNamespaceItem *nsitem,
 						colname),
 				 parser_errposition(pstate, location)));
 
index 1e15ce10b48..4707139a91f 100644
@@ -779,7 +779,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
@@ -867,6 +867,18 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 
 	/*
index 00e7024563e..60acfb587cf 100644
 
 			foreach(lc, rfnodes[idx])
-				filters = lappend(filters, stringToNode((char *) lfirst(lc)));
+				filters = lappend(filters, expand_generated_columns_in_expr(stringToNode((char *) lfirst(lc)), relation, 1));
 
index 6d59a2bb8dc..0829c3b49db 100644
@@ -90,6 +90,7 @@ static List *matchLocks(CmdType event, Relation relation,

 /*
@@ -974,7 +975,8 @@ rewriteTargetListIU(List *targetList,
 		if (att_tup->attgenerated)
 		{
 			/*
-			 * stored generated column will be fixed in executor
+			 * virtual generated column stores a null value; stored generated
+			 * column will be fixed in executor
 			 */
 			new_tle = NULL;
 		}
@@ -2257,6 +2259,30 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		table_close(rel, NoLock);
 	}
 
+	/*
+	 * Expand virtual generated columns of this table
+	 *
+	 * This must be last, since whatever else gets inserted into the query
+	 * above could contain a generated column.
+	 */
+	rt_index = 0;
+	foreach(lc, parsetree->rtable)
+	{
+		RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
+		Relation	rel;
+
+		++rt_index;
+
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		rel = table_open(rte->relid, NoLock);
+
+		parsetree = (Query *) expand_generated_columns_internal((Node *) parsetree, rel, rt_index, rte);
+
+		table_close(rel, NoLock);
+	}
+
 	return parsetree;
 }
 
@@ -4365,6 +4391,89 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
 }

+/*
+		Assert(list_length(tlist) > 0);
+expand_generated_columns_in_expr(Node *node, Relation rel, int rt_index)
+		node = expand_generated_columns_internal(node, rel, rt_index, rte);
index beec6cddbc4..20f607bc330 100644
@@ -157,6 +158,7 @@ RelationBuildPartitionKey(Relation relation)
 		 * in canonical form already (ie, no need for OR-merging or constant
 		 * elimination).
 		 */
+		expr = expand_generated_columns_in_expr(expr, relation, 1);
 		expr = eval_const_expressions(NULL, expr);
 		fix_opfuncids(expr);
 
index c326f687eb4..9c2c47d9cf3 100644
 
index 130b80775db..3b9944ac6db 100644
@@ -16007,6 +16007,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
index ab6c8304913..2b7599d3263 100644
@@ -3631,12 +3631,14 @@
index 6a36c910833..824792075c5 100644

 /*

 
 
index e62ce1b7536..8e9c9980e81 100644
@@ -2739,6 +2739,7 @@ typedef struct Constraint
index 899d64ad55f..efe25d7de0f 100644
@@ -490,6 +490,7 @@ PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
index 1b65cda71cf..8ec879193e2 100644
 
+extern Node *expand_generated_columns_in_expr(Node *node, Relation rel, int rt_index);
+

diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c
index fe719935c67..4cb5969c589 100644
--- a/src/pl/plperl/plperl.c
+++ b/src/pl/plperl/plperl.c
@@ -3048,6 +3048,9 @@ plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc, bool include_generate
 			/* don't include unless requested */
 			if (!include_generated)
 				continue;
+			/* never include virtual columns */
+			if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				continue;
 		}

 

 

 
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index e2ccaa84f3f..bc45412daab 100644
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -3203,6 +3203,9 @@ pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc, bool include_gene
 			/* don't include unless requested */
 			if (!include_generated)
 				continue;
+			/* never include virtual columns */
+			if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				continue;
 		}

 

 

 

+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,

 
 \d gtest1
 
 -- duplicate generated

+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
 
 \d gtest1_1

 

 
 
 -- null values

 -- drop column behavior

 
 \d gtest_child2
 
 \d gtest_child3

 
 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3

 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3

 
 
 \d gtest25

 SELECT * FROM gtest27;
  a | b  | x  

 SELECT * FROM gtest27;
  a | b  | x  
 
 \d gtest29

 \d gtest29

 \d gtest29

 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
 
 \d gtest30_1

 \d gtest30_1

 

 
index 4f38104ba01..b90f48b11d8 100644

 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,

 

 
 \d gtest1
 
 -- duplicate generated

 -- invalid reference

 

 

+CREATE TABLE gtestm (
+  a int PRIMARY KEY,

 

 
 
--- test stored update

 -- null values

 -- drop column behavior
 
 \d gtest10

 -- privileges

 
 
 SET ROLE regress_user11;

 -- check constraints

 
 
--- not-null constraints

 -- index constraints
 
 -- indexes
 
 -- foreign keys

 
 
-DROP TABLE gtest23b;
-DROP TABLE gtest23a;
+--DROP TABLE gtest23b;
+--DROP TABLE gtest23a;

 
 -- domains

 -- partitioning cases

 

 

 \d gtest28*
+
+

 
 # data for initial sync
@@ -42,10 +42,10 @@

 # data to replicate
 
@@ -56,11 +56,11 @@

 
@@ -69,7 +69,7 @@

 $result =
 
 done_testing();

base-commit: 6fd5071909a2886c499871e61127f815fd9bb6a2
-- 
2.46.2

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 5 Nov 2024 17:17:48 +0100

> from Jian He yet; I'll do that next.

New patch version.  I've gone through the whole thread again and looked 
at all the feedback and various bug reports and test cases and made sure 
they are all addressed in the latest patch version.  (I'll send some 
separate messages to respond to some individual messages, but I'm 
keeping the latest patch here.)

From 842faee2964679e183b164f3bf9f418ba4ae460e Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Tue, 5 Nov 2024 16:48:42 +0100
Subject: [PATCH v8] Virtual generated columns

TODO:

contributions by Jian He, Dean Rasheed

Discussion: https://www.postgresql.org/message-id/flat/[email-masked]
---
 contrib/pageinspect/expected/page.out         |  37 +
 contrib/pageinspect/sql/page.sql              |  19 +
 .../postgres_fdw/expected/postgres_fdw.out    |  81 +-
 contrib/postgres_fdw/sql/postgres_fdw.sql     |   9 +-
 doc/src/sgml/catalogs.sgml                    |   4 +-
 doc/src/sgml/ddl.sgml                         |  25 +-
 doc/src/sgml/ref/alter_table.sgml             |  16 +-
 doc/src/sgml/ref/create_foreign_table.sgml    |  11 +-
 doc/src/sgml/ref/create_table.sgml            |  22 +-
 doc/src/sgml/ref/create_trigger.sgml          |   2 +-
 doc/src/sgml/trigger.sgml                     |   4 +
 src/backend/access/common/tupdesc.c           |   3 +
 src/backend/access/heap/heapam_handler.c      |   2 +
 src/backend/catalog/heap.c                    |  13 +-
 src/backend/commands/analyze.c                |   4 +
 src/backend/commands/indexcmds.c              |  29 +-
 src/backend/commands/statscmds.c              |  20 +-
 src/backend/commands/tablecmds.c              | 126 ++-
 src/backend/commands/trigger.c                |  48 +-
 src/backend/executor/execExprInterp.c         |   4 +
 src/backend/executor/execMain.c               |   5 +-
 src/backend/parser/gram.y                     |  15 +-
 src/backend/parser/parse_relation.c           |   6 +-
 src/backend/parser/parse_utilcmd.c            |  14 +-
 src/backend/replication/pgoutput/pgoutput.c   |   3 +-
 src/backend/rewrite/rewriteHandler.c          | 102 +-
 src/backend/utils/cache/partcache.c           |   3 +
 src/backend/utils/cache/relcache.c            |   3 +
 src/bin/pg_dump/pg_dump.c                     |   3 +
 src/bin/pg_dump/t/002_pg_dump.pl              |   6 +-
 src/bin/psql/describe.c                       |   6 +
 src/include/access/tupdesc.h                  |   1 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/pg_attribute.h            |   1 +
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/rewrite/rewriteHandler.h          |   2 +
 src/pl/plperl/expected/plperl_trigger.out     |   7 +-
 src/pl/plperl/plperl.c                        |   3 +
 src/pl/plperl/sql/plperl_trigger.sql          |   3 +-
 src/pl/plpython/expected/plpython_trigger.out |   7 +-
 src/pl/plpython/plpy_typeio.c                 |   3 +
 src/pl/plpython/sql/plpython_trigger.sql      |   3 +-
 src/pl/tcl/expected/pltcl_trigger.out         |  19 +-
 src/pl/tcl/pltcl.c                            |   3 +
 src/pl/tcl/sql/pltcl_trigger.sql              |   3 +-
 .../regress/expected/collate.icu.utf8.out     |  20 +
 .../regress/expected/create_table_like.out    |  23 +-
 src/test/regress/expected/fast_default.out    |  12 +
 .../regress/expected/generated_stored.out     |  77 +-
 ...rated_stored.out => generated_virtual.out} | 868 +++++++++---------
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/sql/collate.icu.utf8.sql     |  14 +
 src/test/regress/sql/create_table_like.sql    |   2 +-
 src/test/regress/sql/fast_default.sql         |  11 +
 src/test/regress/sql/generated_stored.sql     |  38 +-
 ...rated_stored.sql => generated_virtual.sql} | 313 ++++---
 src/test/subscription/t/011_generated.pl      |  38 +-
 58 files changed, 1412 insertions(+), 710 deletions(-)
 copy src/test/regress/expected/{generated_stored.out => generated_virtual.out} (67%)
 copy src/test/regress/sql/{generated_stored.sql => generated_virtual.sql} (69%)

diff --git a/contrib/pageinspect/expected/page.out b/contrib/pageinspect/expected/page.out
index 3fd3869c82a..e42fd9747fd 100644
--- a/contrib/pageinspect/expected/page.out
+++ b/contrib/pageinspect/expected/page.out
@@ -208,6 +208,43 @@ select tuple_data_split('test8'::regclass, t_data, t_infomask, t_infomask2, t_bi
 (1 row)
 
 drop table test8;
+-- check storage of generated columns
+-- stored
+create table test9s (a int not null, b int generated always as (a * 2) stored);
+insert into test9s values (131584);
+select raw_flags, t_bits, t_data
+    from heap_page_items(get_raw_page('test9s', 0)), lateral heap_tuple_infomask_flags(t_infomask, t_infomask2);
+      raw_flags      | t_bits |       t_data       
+---------------------+--------+--------------------
+ {HEAP_XMAX_INVALID} |        | \x0002020000040400
+(1 row)
+
+select tuple_data_split('test9s'::regclass, t_data, t_infomask, t_infomask2, t_bits)
+    from heap_page_items(get_raw_page('test9s', 0));
+       tuple_data_split        
+-------------------------------
+ {"\\x00020200","\\x00040400"}
+(1 row)
+
+drop table test9s;
+-- virtual
+create table test9v (a int not null, b int generated always as (a * 2) virtual);
+insert into test9v values (131584);
+select raw_flags, t_bits, t_data
+    from heap_page_items(get_raw_page('test9v', 0)), lateral heap_tuple_infomask_flags(t_infomask, t_infomask2);
+            raw_flags             |  t_bits  |   t_data   
+----------------------------------+----------+------------
+ {HEAP_HASNULL,HEAP_XMAX_INVALID} | 10000000 | \x00020200
+(1 row)
+
+select tuple_data_split('test9v'::regclass, t_data, t_infomask, t_infomask2, t_bits)
+    from heap_page_items(get_raw_page('test9v', 0));
+   tuple_data_split   
+----------------------
+ {"\\x00020200",NULL}
+(1 row)
+
+drop table test9v;
 -- Failure with incorrect page size
 -- Suppress the DETAIL message, to allow the tests to work across various
 -- page sizes.
diff --git a/contrib/pageinspect/sql/page.sql b/contrib/pageinspect/sql/page.sql
index 346e4ee142c..c75fe1147f6 100644
--- a/contrib/pageinspect/sql/page.sql
+++ b/contrib/pageinspect/sql/page.sql
@@ -84,6 +84,25 @@ CREATE TEMP TABLE test1 (a int, b int);
     from heap_page_items(get_raw_page('test8', 0));
 drop table test8;
 
+select raw_flags, t_bits, t_data
+    from heap_page_items(get_raw_page('test9s', 0)), lateral heap_tuple_infomask_flags(t_infomask, t_infomask2);
+select raw_flags, t_bits, t_data
+    from heap_page_items(get_raw_page('test9v', 0)), lateral heap_tuple_infomask_flags(t_infomask, t_infomask2);

 
 select * from grem1;
- a  | b  
-----+----
-  1 |  2
- 22 | 44
+ a  | b  | c  
+----+----+----
+  1 |  2 |  3
+ 22 | 44 | 66
 (2 rows)
 
 delete from grem1;
 -- test copy from
 copy grem1 from stdin;
 select * from gloc1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 |  
+ 2 | 4 |  
 (2 rows)
 
 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;

 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;
index 964c819a02d..8c2b5038132 100644
 
index f02f67d7b86..2d7c516d569 100644
 
   <para>
 
   <para>
index 36770c012a6..c55ab25e06a 100644
@@ -102,7 +102,7 @@
@@ -266,6 +266,11 @@ <title>Description</title>
 
@@ -278,10 +283,15 @@ <title>Description</title>
       longer apply the generation expression.
      </para>
 
+     <para>

 
    <varlistentry>
 
      <para>
 
      <para>
index 83859bac76f..509624b250c 100644
@@ -724,8 +724,9 @@ <title>Parameters</title>
@@ -901,7 +902,7 @@ <title>Parameters</title>
    </varlistentry>
 
@@ -910,8 +911,11 @@ <title>Parameters</title>
      </para>
 
      <para>
 
      <para>
@@ -2451,9 +2455,9 @@ <title>Multiple Identity Columns</title>
    <title>Generated Columns</title>
 
    <para>
 
index a9abaab9056..46c6c2f126f 100644
 
    <para>

index a8d95e0f1c1..ecc3f599b41 100644
@@ -2047,6 +2047,8 @@ heapam_relation_needs_toast_table(Relation rel)
 
 		if (att->attisdropped)
 			continue;
index c54a543c536..7a954cf6cc3 100644
 
index 9a56de2282f..ba7b28523d9 100644
@@ -1037,6 +1037,10 @@ examine_attribute(Relation onerel, int attnum, Node *index_expr)
 	if (attr->attisdropped)
 		return NULL;
 
index 2f652463e3c..4e4882a3bd3 100644
 
 	/*

 

 			Assert(expr != NULL);

 			k = -1;

 			/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4345b96de5e..22591c233e9 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -2966,6 +2966,15 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 									 errhint("A child table column cannot be generated unless its parent column is.")));
 					}
 
@@ -3237,6 +3246,15 @@ MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const
 
+	if (inhdef->generated && newdef->generated && newdef->generated != inhdef->generated)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+				 errmsg("column \"%s\" inherits from generated column of different kind",
+						inhdef->colname),
+				 errdetail("Parent column is %s, child column is %s.",
+						   inhdef->generated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL",
+						   newdef->generated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL")));
+
 	/*
 	 * If new def has a default, override previous default
 	 */
@@ -6061,7 +6079,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
 		{
 			case CONSTR_CHECK:
 				needscan = true;
-				con->qualstate = ExecPrepareExpr((Expr *) con->qual, estate);
+				con->qualstate = ExecPrepareExpr((Expr *) expand_generated_columns_in_expr(con->qual, newrel ? newrel : oldrel), estate);
 				break;
 			case CONSTR_FOREIGN:
 				/* Nothing to do here */
@@ -7241,7 +7259,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * DEFAULT value outside of the heap.  This may be disabled inside
 		 * AddRelationNewConstraints if the optimization cannot be applied.
 		 */
-		rawEnt->missingMode = (!colDef->generated);
+		rawEnt->missingMode = (colDef->generated != ATTRIBUTE_GENERATED_STORED);
 
 		rawEnt->generated = colDef->generated;
 
@@ -7760,6 +7778,14 @@ ATExecSetNotNull(AlteredTableInfo *tab, Relation rel,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
@@ -8357,7 +8383,18 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 
@@ -8514,17 +8551,30 @@ ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMOD
 
@@ -8667,6 +8717,16 @@ ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa
 
+	/*
@@ -9777,6 +9837,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -11933,7 +12006,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 
 			/* Find or create work queue entry for this table */
 			tab = ATGetQueueEntry(wqueue, rel);
@@ -12994,8 +13067,12 @@ ATPrepAlterColumnType(List **wqueue,
 					   list_make1_oid(rel->rd_rel->reltype),
 					   0);
 
@@ -13068,11 +13145,12 @@ ATPrepAlterColumnType(List **wqueue,
 
@@ -16033,6 +16111,14 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 
@@ -18167,8 +18253,11 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 
 			/*
@@ -18250,9 +18339,12 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 				}
 
 				/*
index 09356e46d16..2228d1a3bce 100644

 /*

 			/*
 
 			/*
@@ -3490,6 +3504,7 @@ TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
 
@@ -6600,3 +6615,34 @@ pg_trigger_depth(PG_FUNCTION_ARGS)
index 30c5a19aad6..1c50197ae72 100644
@@ -2121,6 +2121,10 @@ CheckVarSlotCompatibility(TupleTableSlot *slot, int attnum, Oid vartype)

+		/* Internal error: somebody forgot to expand it. */
+		if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			elog(ERROR, "unexpected virtual generated column reference");
+
 		if (attr->attisdropped)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index cc9a594cba5..5f0d09ab703 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1752,6 +1752,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
 			Expr	   *checkconstr;
 
@@ -2299,7 +2300,9 @@ ExecBuildSlotValueDescription(Oid reloid,
 
index 89fdb94c237..65688ffd363 100644
@@ -628,7 +628,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -776,7 +776,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

 
@@ -3982,7 +3982,7 @@ ColConstraintElem:
 					n->location = @1;
 					$$ = (Node *) n;
 				}
-			| GENERATED generated_when AS '(' a_expr ')' STORED
+			| GENERATED generated_when AS '(' a_expr ')' opt_virtual_or_stored
 				{
 					Constraint *n = makeNode(Constraint);
 
@@ -3990,6 +3990,7 @@ ColConstraintElem:
 					n->generated_when = $2;
 					n->raw_expr = $5;
 					n->cooked_expr = NULL;
+					n->generated_kind = $7;
 					n->location = @1;
 
 					/*
@@ -4036,6 +4037,12 @@ generated_when:
 			| BY DEFAULT	{ $$ = ATTRIBUTE_IDENTITY_BY_DEFAULT; }
 		;
 
+opt_virtual_or_stored:
@@ -17904,6 +17911,7 @@ unreserved_keyword:
@@ -18558,6 +18566,7 @@ bare_label_keyword:

 	/*
index 12c17359063..215c666af7a 100644
@@ -992,7 +993,7 @@ pgoutput_row_filter_init(PGOutputData *data, List *publications,
 				continue;

index 6d59a2bb8dc..ef86dedd9c0 100644

 /*
@@ -2135,6 +2137,15 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 			}
 		}
 
+		/*
+		 * Expand virtual generated columns of this table
+		 *
+		 * FIXME: This should be done after applying RLS policies below, since
+		 * those could also contain virtual columns.  But that currently makes
+		 * some tests fail, so it needs further investigation.
+		 */
+		parsetree = (Query *) expand_generated_columns_internal((Node *) parsetree, rel, rt_index, rte);
+
 		table_close(rel, NoLock);
 	}
 
@@ -4365,6 +4376,95 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
 }

+/*
 
index 5bbb654a5db..8d3b08475c3 100644
@@ -590,6 +590,8 @@ RelationBuildTupleDesc(Relation relation)
 
@@ -686,6 +688,7 @@ RelationBuildTupleDesc(Relation relation)
 	 */
+		constr->has_generated_virtual ||
index d8c6330732e..25480e1b126 100644
@@ -16020,6 +16020,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
index ac60829d686..f571a259b06 100644
index 363a66e7185..d61c4ae7999 100644
@@ -2085,6 +2085,12 @@ describeOneTableDetails(const char *schemaname,

 /*
index d6a2c791290..33b950ad6df 100644

 
 
index 0d96db56386..15d898ef5dc 100644
@@ -2765,6 +2765,7 @@ typedef struct Constraint
 	List	   *keys;			/* String nodes naming referenced key
 								 * column(s); for UNIQUE/PK/NOT NULL */

 
diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c
index 1b1677e333b..ebf55fe663c 100644
--- a/src/pl/plperl/plperl.c
+++ b/src/pl/plperl/plperl.c
@@ -3047,6 +3047,9 @@ plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc, bool include_generate
 			/* don't include unless requested */
 			if (!include_generated)
 				continue;
+			/* never include virtual columns */
+			if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				continue;
 		}

 

 

 
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index 80de0db40de..0de48148b47 100644
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -3202,6 +3202,9 @@ pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc, bool include_gene
 			/* don't include unless requested */
 			if (!include_generated)
 				continue;
+			/* never include virtual columns */
+			if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				continue;
 		}

 
 CREATE VIEW trigger_test_view AS SELECT i, v FROM trigger_test;
diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out
index faa376e060c..5a94ce13afa 100644
--- a/src/test/regress/expected/collate.icu.utf8.out
+++ b/src/test/regress/expected/collate.icu.utf8.out
@@ -2054,6 +2054,26 @@ SELECT (SELECT count(*) FROM test33_0) <> (SELECT count(*) FROM test33_1);
  t
 (1 row)
 
+-- virtual generated columns
+CREATE TABLE t5 (
+    a int,
+    b text collate "C",
+    c text collate "C" GENERATED ALWAYS AS (b COLLATE case_insensitive)
+);
+INSERT INTO t5 (a, b) values (1, 'D1'), (2, 'D2'), (3, 'd1');
+-- Collation of c should be the one defined for the column ("C"), not
+-- the one of the generation expression.  (Note that we cannot just
+-- test with, say, using COLLATION FOR, because the collation of
+-- function calls is already determined in the parser before
+-- rewriting.)
+SELECT * FROM t5 ORDER BY c ASC, a ASC;
+ a | b  | c  
+---+----+----
+ 1 | D1 | D1
+ 2 | D2 | D2
+ 3 | d1 | d1
+(3 rows)
+
 -- cleanup
 RESET search_path;
 SET client_min_messages TO warning;
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index 6bfc6d040ff..97157dc635b 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -113,19 +113,20 @@ SELECT * FROM test_like_id_3;  -- identity was copied and applied
 (1 row)

 

 
 
 DROP TABLE test_like_gen_1, test_like_gen_2, test_like_gen_3;
diff --git a/src/test/regress/expected/fast_default.out b/src/test/regress/expected/fast_default.out
index 59365dad964..272b57e48cd 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -58,6 +58,18 @@ ALTER TABLE has_volatile ADD col2 int DEFAULT 1;
 ALTER TABLE has_volatile ADD col3 timestamptz DEFAULT current_timestamp;
 ALTER TABLE has_volatile ADD col4 int DEFAULT (random() * 10000)::int;
 NOTICE:  rewriting table has_volatile for reason 2
+-- virtual generated columns don't need a rewrite
+ALTER TABLE has_volatile ADD col5 int GENERATED ALWAYS AS (tableoid::int + col2) VIRTUAL;
+ALTER TABLE has_volatile ALTER COLUMN col5 TYPE float8;
+ALTER TABLE has_volatile ALTER COLUMN col5 TYPE numeric;
+ALTER TABLE has_volatile ALTER COLUMN col5 TYPE numeric;
+-- here, we do need a rewrite
+ALTER TABLE has_volatile ALTER COLUMN col1 SET DATA TYPE float8,
+  ADD COLUMN col6 float8 GENERATED ALWAYS AS (col1 * 4) VIRTUAL;
+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
 -- 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_stored.out b/src/test/regress/expected/generated_stored.out
index 8ea8a3a92d2..766cb4fc0af 100644
--- a/src/test/regress/expected/generated_stored.out
+++ b/src/test/regress/expected/generated_stored.out
@@ -1,5 +1,6 @@
+-- keep these tests aligned with generated_virtual.sql
 -- sanity check of system catalog
-SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v');
  attrelid | attname | attgenerated 
 ----------+---------+--------------
 (0 rows)
@@ -128,6 +129,24 @@ SELECT * FROM gtest1 ORDER BY a;
  4 | 8
 (4 rows)
 
+SELECT gtest1 FROM gtest1 ORDER BY a;  -- whole-row reference
+ gtest1 
+--------
+ (1,2)
+ (2,4)
+ (3,6)
+ (4,8)
+(4 rows)
+
+SELECT a, (SELECT gtest1.b) FROM gtest1 ORDER BY a;  -- sublink
+ a | b 
+---+---
+ 1 | 2
+ 2 | 4
+ 3 | 6
+ 4 | 8
+(4 rows)
+
 DELETE FROM gtest1 WHERE a >= 3;
 UPDATE gtest1 SET b = DEFAULT WHERE a = 1;
 UPDATE gtest1 SET b = 11 WHERE a = 1;  -- error
@@ -217,6 +236,27 @@ SELECT * FROM gtestm ORDER BY id;
   2 | 20 | 200 | 40 | 400
 (2 rows)
 
+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
+  b int GENERATED ALWAYS AS (a * 2) STORED
+);
+INSERT INTO gtestm (a) SELECT g FROM generate_series(1, 10) g;
+MERGE INTO gtestm t USING gtestm AS s ON 2 * t.a = s.b WHEN MATCHED THEN DELETE RETURNING *;
+ a  | b  | a  | b  
+----+----+----+----
+  1 |  2 |  1 |  2
+  2 |  4 |  2 |  4
+  3 |  6 |  3 |  6
+  4 |  8 |  4 |  8
+  5 | 10 |  5 | 10
+  6 | 12 |  6 | 12
+  7 | 14 |  7 | 14
+  8 | 16 |  8 | 16
+  9 | 18 |  9 | 18
+ 10 | 20 | 10 | 20
+(10 rows)
+
 DROP TABLE gtestm;
 -- views
 CREATE VIEW gtest1v AS SELECT * FROM gtest1;
@@ -312,6 +352,10 @@ ERROR:  column "b" inherits from generated column but specifies default
 CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS IDENTITY) INHERITS (gtest1);  -- error
 NOTICE:  merging column "b" with inherited definition
 ERROR:  column "b" inherits from generated column but specifies identity
+CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) VIRTUAL) INHERITS (gtest1);  -- error
+NOTICE:  merging column "b" with inherited definition
+ERROR:  column "b" inherits from generated column of different kind
+DETAIL:  Parent column is STORED, child column is VIRTUAL.
 CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) STORED) INHERITS (gtest1);  -- ok, overrides parent
 NOTICE:  merging column "b" with inherited definition
 \d+ gtestx
@@ -762,6 +806,11 @@ CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a *
 INSERT INTO gtest24 (a) VALUES (4);  -- ok
 INSERT INTO gtest24 (a) VALUES (6);  -- error
 ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+CREATE TYPE gtestdomain1range AS range (subtype = gtestdomain1);
+CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) STORED);
+INSERT INTO gtest24r (a) VALUES (4);  -- ok
+INSERT INTO gtest24r (a) VALUES (6);  -- error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
 -- 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);
@@ -792,6 +841,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
     f3 WITH OPTIONS GENERATED ALWAYS AS IDENTITY  -- error
 ) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 ERROR:  identity columns are not supported on partitions
+CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
+    f3 GENERATED ALWAYS AS (f2 * 2) VIRTUAL  -- error
+) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
+ERROR:  column "f3" inherits from generated column of different kind
+DETAIL:  Parent column is STORED, child column is VIRTUAL.
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
 ERROR:  column "f3" in child table must be a generated column
@@ -805,6 +859,11 @@ ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09
 ERROR:  table "gtest_child3" being attached contains an identity column "f3"
 DETAIL:  The new partition may not contain an identity column.
 DROP TABLE gtest_child3;
+CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) VIRTUAL);
+ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
+ERROR:  column "f3" inherits from generated column of different kind
+DETAIL:  Parent column is STORED, child column is VIRTUAL.
+DROP TABLE gtest_child3;
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) STORED);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 \d gtest_child
@@ -1092,9 +1151,9 @@ SELECT * FROM gtest29;
 ALTER TABLE gtest29 ALTER COLUMN a SET EXPRESSION AS (a * 3);  -- error
 ERROR:  column "a" of relation "gtest29" is not a generated column
 ALTER TABLE gtest29 ALTER COLUMN a DROP EXPRESSION;  -- error
-ERROR:  column "a" of relation "gtest29" is not a stored generated column
+ERROR:  column "a" of relation "gtest29" is not a 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
+NOTICE:  column "a" of relation "gtest29" is not a generated column, skipping
 -- Change the expression
 ALTER TABLE gtest29 ALTER COLUMN b SET EXPRESSION AS (a * 3);
 SELECT * FROM gtest29;
@@ -1188,6 +1247,18 @@ Inherits: gtest30
 
 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION;  -- error
 ERROR:  cannot drop generation expression from inherited column
+-- composite type dependencies
+CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text);
+CREATE TABLE gtest31_2 (x int, y gtest31_1);
+ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
+ERROR:  cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type
+DROP TABLE gtest31_1, gtest31_2;
+-- Check it for a partitioned table, too
+CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text) PARTITION BY LIST (a);
+CREATE TABLE gtest31_2 (x int, y gtest31_1);
+ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
+ERROR:  cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type
+DROP TABLE gtest31_1, gtest31_2;
 -- triggers
 CREATE TABLE gtest26 (
     a int PRIMARY KEY,
diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_virtual.out
similarity index 67%
copy from src/test/regress/expected/generated_stored.out
copy to src/test/regress/expected/generated_virtual.out
index 8ea8a3a92d2..8864fe8bb18 100644
--- a/src/test/regress/expected/generated_stored.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1,15 +1,16 @@
+-- keep these tests aligned with generated_stored.sql
 -- sanity check of system catalog
-SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v');
  attrelid | attname | attgenerated 
 ----------+---------+--------------
 (0 rows)
 
@@ -18,89 +19,89 @@ SELECT table_name, column_name, column_default, is_nullable, is_generated, gener

 \d gtest1
 
 -- duplicate generated
@@ -128,6 +129,24 @@ SELECT * FROM gtest1 ORDER BY a;
  4 | 8
 (4 rows)
 
+SELECT gtest1 FROM gtest1 ORDER BY a;  -- whole-row reference
+ gtest1 
+--------
+ (1,2)
+ (2,4)
+ (3,6)
+ (4,8)
+(4 rows)
+
+SELECT a, (SELECT gtest1.b) FROM gtest1 ORDER BY a;  -- sublink
+ a | b 
+---+---
+ 1 | 2
+ 2 | 4
+ 3 | 6
+ 4 | 8
+(4 rows)
+
 DELETE FROM gtest1 WHERE a >= 3;
 UPDATE gtest1 SET b = DEFAULT WHERE a = 1;
 UPDATE gtest1 SET b = 11 WHERE a = 1;  -- error
@@ -153,16 +172,10 @@ SELECT a, b FROM gtest1 WHERE b = 4 ORDER BY a;
  2 | 4
 (1 row)
 
@@ -203,8 +216,8 @@ CREATE TABLE gtestm (
@@ -217,6 +230,27 @@ SELECT * FROM gtestm ORDER BY id;
   2 | 20 | 200 | 40 | 400
 (2 rows)
 
+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
@@ -273,11 +307,11 @@ SELECT * FROM gtest1_1;
 (0 rows)
 
 \d gtest1_1
 
 INSERT INTO gtest1_1 VALUES (4);
@@ -296,12 +330,12 @@ SELECT * FROM gtest1;
 
@@ -312,23 +346,42 @@ ERROR:  column "b" inherits from generated column but specifies default
 
@@ -341,28 +394,28 @@ CREATE TABLE gtest1_y () INHERITS (gtest1, gtesty);  -- error
 
@@ -379,8 +432,8 @@ TABLE gtestc;
 
@@ -401,7 +454,7 @@ SELECT * FROM gtest3 ORDER BY a;
     |   
 (4 rows)
 
@@ -466,7 +519,7 @@ SELECT * FROM gtest3 ORDER BY a;
 (4 rows)
 
 -- null values
-CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED);
+CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) VIRTUAL);
 INSERT INTO gtest2 VALUES (1);
 SELECT * FROM gtest2;
  a | b 
@@ -475,7 +528,7 @@ SELECT * FROM gtest2;
 (1 row)
 
@@ -490,7 +543,7 @@ DROP TABLE gtest_varlena;
@@ -505,11 +558,11 @@ DROP TYPE double_int;
@@ -518,7 +571,7 @@ SELECT * FROM gtest_tableoid;
 (2 rows)
 
 -- drop column behavior
@@ -526,30 +579,31 @@ HINT:  Use DROP ... CASCADE to drop the dependent objects too.
 
@@ -558,231 +612,142 @@ SELECT a, c FROM gtest11s;  -- allowed
 
@@ -792,6 +757,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -806,32 +776,37 @@ ERROR:  table "gtest_child3" being attached contains an identity column "f3"
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) STORED);
+ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
+DROP TABLE gtest_child3;
 
 \d gtest_child2
 
 \d gtest_child3
 
@@ -842,103 +817,127 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
 
+SELECT tableoid::regclass, * FROM gtest_child ORDER BY 1, 2, 3;
+  tableoid   |     f1     | f2 | f3 
+-------------+------------+----+----
+ gtest_child | 07-15-2016 |  1 |  2
+ gtest_child | 07-15-2016 |  2 |  4
+(2 rows)
+
+SELECT tableoid::regclass, * FROM gtest_child2 ORDER BY 1, 2, 3;  -- uses child's generation expression, not parent's
+   tableoid   |     f1     | f2 | f3 
+--------------+------------+----+----
+ gtest_child2 | 08-15-2016 |  3 | 66
+(1 row)
+
+SELECT tableoid::regclass, * FROM gtest_child3 ORDER BY 1, 2, 3;
+ tableoid | f1 | f2 | f3 
+----------+----+----+----
+(0 rows)
+
 UPDATE gtest_parent SET f1 = f1 + 60 WHERE f2 = 1;
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
    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
+ gtest_child2 | 08-15-2016 |  3 |  6
+ gtest_child3 | 09-13-2016 |  1 |  2
 (3 rows)

 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3

 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3
 
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
@@ -951,54 +950,54 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;

 
 \d gtest25
 
@@ -1006,7 +1005,7 @@ Indexes:
 CREATE TABLE gtest27 (
     a int,
     b int,
@@ -1014,12 +1013,12 @@ ERROR:  cannot alter type of a column used by a generated column
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1033,20 +1032,19 @@ ERROR:  cannot specify USING when altering type of generated column
 
@@ -1055,12 +1053,12 @@ ALTER TABLE gtest27
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1072,7 +1070,7 @@ SELECT * FROM gtest27;
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -1083,115 +1081,138 @@ SELECT * FROM gtest29;
 (2 rows)
 
 \d gtest29

 \d gtest29

 \d gtest29

 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
 
 \d gtest30_1

 \d gtest30_1
 
 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION;  -- error
 ERROR:  cannot drop generation expression from inherited column
+-- 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);
+ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
+ERROR:  cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type
+DROP TABLE gtest31_1, gtest31_2;
+-- Check it for a partitioned table, too
+CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') VIRTUAL, c text) PARTITION BY LIST (a);
+CREATE TABLE gtest31_2 (x int, y gtest31_1);
+ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
+ERROR:  cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type
+DROP TABLE gtest31_1, gtest31_2;
 -- triggers
 CREATE TABLE gtest26 (
     a int PRIMARY KEY,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE FUNCTION gtest_trigger_func() RETURNS trigger
   LANGUAGE plpgsql
@@ -1244,7 +1265,7 @@ CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26
   EXECUTE PROCEDURE gtest_trigger_func();
 INSERT INTO gtest26 (a) VALUES (-2), (0), (3);
 INFO:  gtest2: BEFORE: new = (-2,)
-INFO:  gtest4: AFTER: new = (-2,-4)
+INFO:  gtest4: AFTER: new = (-2,)
 SELECT * FROM gtest26 ORDER BY a;
  a  | b  
 ----+----
@@ -1254,12 +1275,12 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
@@ -1269,8 +1290,8 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
@@ -1281,6 +1302,8 @@ SELECT * FROM gtest26 ORDER BY a;
@@ -1294,9 +1317,11 @@ $$;
@@ -1320,14 +1345,13 @@ CREATE TRIGGER gtest12_03 BEFORE UPDATE ON gtest26
 
@@ -1335,22 +1359,22 @@ CREATE TABLE gtest28a (

index 81e4222d26a..782d7d11dcb 100644
 
+# TODO: find a place for this (above group is full)
+test: generated_virtual
+
 # ----------
 # Additional BRIN tests
 # ----------
diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql
index 80f28a97d78..57cc11a3eed 100644
--- a/src/test/regress/sql/collate.icu.utf8.sql
+++ b/src/test/regress/sql/collate.icu.utf8.sql
@@ -796,6 +796,20 @@ CREATE TABLE test33_1 PARTITION OF test33 FOR VALUES WITH (MODULUS 2, REMAINDER
 -- they end up in the same partition (but it's platform-dependent which one)
 SELECT (SELECT count(*) FROM test33_0) <> (SELECT count(*) FROM test33_1);
 
+-- virtual generated columns
+CREATE TABLE t5 (
+    a int,
+    b text collate "C",
+    c text collate "C" GENERATED ALWAYS AS (b COLLATE case_insensitive)
+);
+INSERT INTO t5 (a, b) values (1, 'D1'), (2, 'D2'), (3, 'd1');
+-- Collation of c should be the one defined for the column ("C"), not
+-- the one of the generation expression.  (Note that we cannot just
+-- test with, say, using COLLATION FOR, because the collation of
+-- function calls is already determined in the parser before
+-- rewriting.)
+SELECT * FROM t5 ORDER BY c ASC, a ASC;
+
 
 -- cleanup
 RESET search_path;
 
-CREATE TABLE test_like_gen_1 (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE test_like_gen_1 (a int, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (a * 3) VIRTUAL);
 \d test_like_gen_1
 INSERT INTO test_like_gen_1 (a) VALUES (1);
 SELECT * FROM test_like_gen_1;
diff --git a/src/test/regress/sql/fast_default.sql b/src/test/regress/sql/fast_default.sql
index dc9df78a35d..6e7f37b17b2 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -66,6 +66,17 @@ CREATE EVENT TRIGGER has_volatile_rewrite
 ALTER TABLE has_volatile ADD col3 timestamptz DEFAULT current_timestamp;
 ALTER TABLE has_volatile ADD col4 int DEFAULT (random() * 10000)::int;
 
+-- virtual generated columns don't need a rewrite
+ALTER TABLE has_volatile ADD col5 int GENERATED ALWAYS AS (tableoid::int + col2) VIRTUAL;
+ALTER TABLE has_volatile ALTER COLUMN col5 TYPE float8;
+ALTER TABLE has_volatile ALTER COLUMN col5 TYPE numeric;
+ALTER TABLE has_volatile ALTER COLUMN col5 TYPE numeric;
+-- here, we do need a rewrite
+ALTER TABLE has_volatile ALTER COLUMN col1 SET DATA TYPE float8,
+  ADD COLUMN col6 float8 GENERATED ALWAYS AS (col1 * 4) VIRTUAL;
+-- stored generated columns need a rewrite
+ALTER TABLE has_volatile ADD col7 int GENERATED ALWAYS AS (55) stored;
+

 -- Test a large sample of different datatypes
diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql
index c18e0e1f655..02fa17386c4 100644
--- a/src/test/regress/sql/generated_stored.sql
+++ b/src/test/regress/sql/generated_stored.sql
@@ -1,5 +1,8 @@
+-- keep these tests aligned with generated_virtual.sql
+
+
 -- sanity check of system catalog
-SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v');

 CREATE SCHEMA generated_stored_tests;
@@ -60,6 +63,8 @@ CREATE TABLE gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT AS (a *
 INSERT INTO gtest1 VALUES (3, DEFAULT), (4, DEFAULT);  -- ok
 
 SELECT * FROM gtest1 ORDER BY a;
+SELECT gtest1 FROM gtest1 ORDER BY a;  -- whole-row reference
+SELECT a, (SELECT gtest1.b) FROM gtest1 ORDER BY a;  -- sublink
 DELETE FROM gtest1 WHERE a >= 3;
 
 UPDATE gtest1 SET b = DEFAULT WHERE a = 1;
@@ -103,6 +108,14 @@ CREATE TABLE gtestm (
 SELECT * FROM gtestm ORDER BY id;
 DROP TABLE gtestm;
 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
+  b int GENERATED ALWAYS AS (a * 2) STORED
+);
+INSERT INTO gtestm (a) SELECT g FROM generate_series(1, 10) g;
+MERGE INTO gtestm t USING gtestm AS s ON 2 * t.a = s.b WHEN MATCHED THEN DELETE RETURNING *;
+DROP TABLE gtestm;
+
 -- views
 CREATE VIEW gtest1v AS SELECT * FROM gtest1;
 SELECT * FROM gtest1v;
@@ -142,6 +155,7 @@ CREATE TABLE gtest_normal_child (a int, b int GENERATED ALWAYS AS (a * 2) STORED
 -- test inheritance mismatches between parent and child
 CREATE TABLE gtestx (x int, b int DEFAULT 10) INHERITS (gtest1);  -- error
 CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS IDENTITY) INHERITS (gtest1);  -- error
+CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) VIRTUAL) INHERITS (gtest1);  -- error
 CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) STORED) INHERITS (gtest1);  -- ok, overrides parent
 \d+ gtestx
 
@@ -390,6 +404,10 @@ CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10);
 
 -- typed tables (currently not supported)
 CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint);
@@ -417,6 +435,9 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
 CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
     f3 WITH OPTIONS GENERATED ALWAYS AS IDENTITY  -- error
 ) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
+CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
+    f3 GENERATED ALWAYS AS (f2 * 2) VIRTUAL  -- error
+) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
 DROP TABLE gtest_child3;
@@ -426,6 +447,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint DEFAULT 42);
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS IDENTITY);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
 DROP TABLE gtest_child3;
+CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) VIRTUAL);
+ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
+DROP TABLE gtest_child3;
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) STORED);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 \d gtest_child
@@ -549,6 +573,18 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 \d gtest30_1
 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION;  -- error
 
+-- composite type dependencies
+CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text);
+CREATE TABLE gtest31_2 (x int, y gtest31_1);
+ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
+DROP TABLE gtest31_1, gtest31_2;
+
+-- Check it for a partitioned table, too
+CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text) PARTITION BY LIST (a);
+CREATE TABLE gtest31_2 (x int, y gtest31_1);
+ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
+DROP TABLE gtest31_1, gtest31_2;
+
 -- triggers
 CREATE TABLE gtest26 (
     a int PRIMARY KEY,
diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_virtual.sql
similarity index 69%
copy from src/test/regress/sql/generated_stored.sql
copy to src/test/regress/sql/generated_virtual.sql
index c18e0e1f655..cb727c44cfb 100644
--- a/src/test/regress/sql/generated_stored.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -1,55 +1,58 @@
+-- keep these tests aligned with generated_stored.sql
+
+
 -- sanity check of system catalog
-SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v');

 

 \d gtest1
 
 -- duplicate generated

 -- invalid reference

 

 
 INSERT INTO gtest1 VALUES (1);
 INSERT INTO gtest1 VALUES (2, DEFAULT);  -- ok
@@ -60,6 +63,8 @@ CREATE TABLE gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT AS (a *
 INSERT INTO gtest1 VALUES (3, DEFAULT), (4, DEFAULT);  -- ok
 
 SELECT * FROM gtest1 ORDER BY a;
+SELECT gtest1 FROM gtest1 ORDER BY a;  -- whole-row reference
+SELECT a, (SELECT gtest1.b) FROM gtest1 ORDER BY a;  -- sublink
 DELETE FROM gtest1 WHERE a >= 3;
 
 UPDATE gtest1 SET b = DEFAULT WHERE a = 1;
@@ -70,7 +75,7 @@ CREATE TABLE gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT AS (a *
 SELECT a, b, b * 2 AS b2 FROM gtest1 ORDER BY a;
 SELECT a, b FROM gtest1 WHERE b = 4 ORDER BY a;
 
@@ -93,8 +98,8 @@ CREATE TABLE gtestm (
@@ -103,6 +108,14 @@ CREATE TABLE gtestm (
 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
@@ -134,22 +147,26 @@ CREATE TABLE gtest1_1 () INHERITS (gtest1);

 
 
 -- test multiple inheritance mismatches
@@ -161,28 +178,28 @@ CREATE TABLE gtesty (x int, b int);
 CREATE TABLE gtest1_y () INHERITS (gtest1, gtesty);  -- error
 DROP TABLE gtesty;

 
--- test stored update
 
@@ -222,12 +239,12 @@ CREATE TABLE gtest3a (a text, b text GENERATED ALWAYS AS (a || '+' || a) STORED)
 
 -- null values
 
@@ -237,7 +254,7 @@ CREATE TABLE gtest_varlena (a varchar, b varchar GENERATED ALWAYS AS (a) STORED)
@@ -248,168 +265,172 @@ CREATE TABLE gtest4 (
 
 -- drop column behavior
 
 \d gtest10

 -- privileges

 
 
 SET ROLE regress_user11;

 -- check constraints

 
 
--- not-null constraints

 -- index constraints
 
 -- indexes
 
 -- foreign keys

 
 
-DROP TABLE gtest23b;
-DROP TABLE gtest23a;
+--DROP TABLE gtest23b;
+--DROP TABLE gtest23a;

 
 -- domains

 -- partitioning cases
 
-CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f1);
+CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE (f1);
 CREATE TABLE gtest_child PARTITION OF gtest_parent
   FOR VALUES FROM ('2016-07-01') TO ('2016-08-01');  -- inherits gen expr
 CREATE TABLE gtest_child2 PARTITION OF gtest_parent (
-    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 22) STORED  -- overrides gen expr
+    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 22) VIRTUAL  -- overrides gen expr
 ) FOR VALUES FROM ('2016-08-01') TO ('2016-09-01');
 CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
     f3 DEFAULT 42  -- error
@@ -417,6 +438,9 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
 CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
     f3 WITH OPTIONS GENERATED ALWAYS AS IDENTITY  -- error
 ) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
+CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
+    f3 GENERATED ALWAYS AS (f2 * 2) STORED  -- error
+) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
 DROP TABLE gtest_child3;
@@ -427,6 +451,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
 DROP TABLE gtest_child3;
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) STORED);
+ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
+DROP TABLE gtest_child3;
+CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) VIRTUAL);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 \d gtest_child
 \d gtest_child2
@@ -435,6 +462,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 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, 2, 3;
+SELECT tableoid::regclass, * FROM gtest_child ORDER BY 1, 2, 3;
+SELECT tableoid::regclass, * FROM gtest_child2 ORDER BY 1, 2, 3;  -- uses child's generation expression, not parent's
+SELECT tableoid::regclass, * FROM gtest_child3 ORDER BY 1, 2, 3;
 UPDATE gtest_parent SET f1 = f1 + 60 WHERE f2 = 1;
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
 
@@ -457,21 +487,21 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 -- we leave these tables around for purposes of testing dump/reload/upgrade

 
@@ -479,7 +509,7 @@ CREATE TABLE gtest25 (a int PRIMARY KEY);
 CREATE TABLE gtest27 (
     a int,
     b int,
-    x int GENERATED ALWAYS AS ((a + b) * 2) STORED
+    x int GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL
 );
 INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11);
 ALTER TABLE gtest27 ALTER COLUMN a TYPE text;  -- error
@@ -493,7 +523,7 @@ CREATE TABLE gtest27 (
   DROP COLUMN x,
   ALTER COLUMN a TYPE bigint,
   ALTER COLUMN b TYPE bigint,
-  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) STORED;
+  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL;
 \d gtest27
 -- Ideally you could just do this, but not today (and should x change type?):
 ALTER TABLE gtest27
@@ -505,7 +535,7 @@ CREATE TABLE gtest27 (
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -532,7 +562,7 @@ CREATE TABLE gtest29 (
 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE gtest30 ALTER COLUMN b DROP EXPRESSION;
@@ -541,7 +571,7 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 DROP TABLE gtest30 CASCADE;
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE ONLY gtest30 ALTER COLUMN b DROP EXPRESSION;  -- error
@@ -549,10 +579,22 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 \d gtest30_1
 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION;  -- error
 
+-- 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);
+ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
+DROP TABLE gtest31_1, gtest31_2;
+
+-- Check it for a partitioned table, too
+CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') VIRTUAL, c text) PARTITION BY LIST (a);
+CREATE TABLE gtest31_2 (x int, y gtest31_1);
+ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
+DROP TABLE gtest31_1, gtest31_2;
+
 -- triggers
 CREATE TABLE gtest26 (
     a int PRIMARY KEY,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 
 CREATE FUNCTION gtest_trigger_func() RETURNS trigger
@@ -614,6 +656,9 @@ CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26
 DROP TRIGGER gtest2 ON gtest26;
 DROP TRIGGER gtest3 ON gtest26;
 
@@ -667,7 +712,7 @@ CREATE TABLE gtest28a (

 
 
 # data for initial sync
@@ -42,10 +42,10 @@

 # data to replicate
 
@@ -56,11 +56,11 @@

 
@@ -69,7 +69,7 @@

 $result =
 
 done_testing();

base-commit: 7d85d87f4d5c35fd5b2d38adaef63dfbfa542ccc
-- 
2.47.0

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 5 Nov 2024 17:19:01 +0100

> GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) [VIRTUAL]

I have addressed this in patch v8.

>          relation->rd_rel->relchecks > 0)

fixed in v8

> since it already has event triggers and trigger functions, we don't

Also added in v8.

Thanks!

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 5 Nov 2024 17:21:42 +0100

> ERROR:  foreign key constraints on virtual generated columns are not supported

I think the existing behavior is fine.  The first message is about 
something that is invalid anyway.  The second message is just that 
something is not supported yet.  If we end up implementing, then users 
will get the first message.

> expand information on t_infomask, t_bits information.

added to v8 patch

> make the transient TupleDesc->TupleConstr three bool flags more accurate.

I don't think we need that.  At the time this is used, the generation 
expressions are not added to the table yet.  Note that stored generated 
columns are not dealt with here either.  If there is a bug, then we can 
fix it, but if not, then I'd rather keep the code simpler.

---

From: Dean Rasheed <[email-masked]>
Date: Thu, 7 Nov 2024 09:35:42 +0000

On Tue, 5 Nov 2024 at 16:17, Peter Eisentraut <[email-masked]> wrote:
> New patch version.

What happened with the RLS support? It looks like you moved the code
to expand virtual generated columns back to the first loop in
fireRIRrules(), which doesn't work because RLS policies might contain
references to virtual generated columns.

In the v7 patch, it was done in a separate loop, after the RLS policy
loop, which I thought was fine, except that I didn't like having a
whole new loop, opening and closing all the relations in the query.
Was there some other problem with that approach?

What I originally had in mind was doing it at the end of the RLS
policy loop, rather than adding another loop, as in the attached delta
patch. This passes all the current tests, and appears to work fine in
the new tests with RLS policies referring to virtual generated
columns.

Regards,
Dean

---

From: Dean Rasheed <[email-masked]>
Date: Thu, 7 Nov 2024 10:02:34 +0000

> New patch version.

In expand_generated_columns_in_expr():

+        /*

It seems a bit messy to be doing these two passes in
expand_generated_columns_in_expr(), when it is only needed for
triggers. I think it was better the way it was in the v7 patch,
passing rt_index to expand_generated_columns_in_expr(), so that
TriggerEnabled() did this:

+            tgqual = (Node *)
expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc,
PRS2_OLD_VARNO);
+            tgqual = (Node *)
expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc,
PRS2_NEW_VARNO);

Regards,
Dean

---

From: Amit Kapila <[email-masked]>
Date: Sun, 10 Nov 2024 08:46:37 +0530

> keeping the latest patch here.)

I have tried to analyze this patch's interaction with logical
replication. The patch allows virtual generated columns in row filters
and column lists. But for the column list, it doesn't seem to be
computing the correct value whereas for the row filter, it is working
due to the following change:

@@ -992,7 +993,7 @@ pgoutput_row_filter_init(PGOutputData *data, List
*publications,
  continue;

  foreach(lc, rfnodes[idx])
- filters = lappend(filters, stringToNode((char *) lfirst(lc)));
+ filters = lappend(filters,
expand_generated_columns_in_expr(stringToNode((char *) lfirst(lc)),
relation));

The possible idea to replicate virtual generated columns is to compute
the corresponding expression before sending the data to the client. If
we can allow it in the row filter than why not to publish it as well.
To allow updates, we need to ensure that the replica identity should
include all columns referenced by the generated expression. For
example, if the generated column is defined as generated always as (c1
+ c2), the replica identity must include both c1 and c2.

Now, if we can't support the replication of virtual generated columns
due to some reason then we can mention in docs for
publish_generated_columns that it is used only to replicate STORED
generated columns but if we can support it then the
publish_generated_columns can accept string values like 'stored',
'virtual', 'all'.

Thoughts?

-- 
With Regards,
Amit Kapila.

---

From: vignesh C <[email-masked]>
Date: Mon, 11 Nov 2024 11:21:25 +0530

> keeping the latest patch here.)

The patch needs to be rebased due to a recent commit 14e87ffa5c5. I
have verified the behavior of logical replication of row filters on
the virtual generated column, and everything appears to be functioning
as expected. One suggestion would be to add a test case for the row
filter on a virtual generated column.

Regards,
Vignesh

---

From: jian he <[email-masked]>
Date: Mon, 11 Nov 2024 19:37:50 +0800

> keeping the latest patch here.)

just quickly note the not good error message before you rebase.

src7=# create domain d_fail as int4 constraint cc GENERATED ALWAYS AS (2) ;
ERROR:  unrecognized constraint subtype: 4
src7=# create domain d_fail as int4 constraint cc GENERATED ALWAYS AS
(2) stored;
ERROR:  unrecognized constraint subtype: 4
src7=# create domain d_fail as int4 constraint cc GENERATED ALWAYS AS
(2) virtual;
ERROR:  unrecognized constraint subtype: 4

reading gram.y, typedef struct Constraint seems cannot distinguish, we
are creating a domain or create table.
I cannot found a way to error out in gram.y.

so we have to error out at DefineDomain.

---

From: jian he <[email-masked]>
Date: Tue, 12 Nov 2024 16:49:51 +0800

> > keeping the latest patch here.)

RelationBuildPartitionKey
if (!isnull)
{
        char       *exprString;
        Node       *expr;
        exprString = TextDatumGetCString(datum);
        expr = stringToNode(exprString);
        pfree(exprString);
        expr = expand_generated_columns_in_expr(expr, relation);
}
no need expand_generated_columns_in_expr?
in ComputePartitionAttrs, we already forbidden generated columns to be
part of the partition key.

check_modified_virtual_generated, we can replace fastgetattr to
heap_attisnull? like:
            // bool        isnull;
            // fastgetattr(tuple, i + 1, tupdesc, &isnull);
            // if (!isnull)
            //     ereport(ERROR,
            //             (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
            //              errmsg("trigger modified virtual generated
column value")));
            if (!heap_attisnull(tuple, i+1, tupdesc))
                ereport(ERROR,
                        (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
                         errmsg("trigger modified virtual generated
column value")));

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 12 Nov 2024 17:06:37 +0100

> Was there some other problem with that approach?

I have no idea what happened there.  I must have used the wrong patch 
version at some point.  I have applied your patch to fix that back up. 
Also thanks for the RLS test cases.
From 90ee6fb718961d651aede5da998bdeaddbb5cf09 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Tue, 12 Nov 2024 12:58:16 +0100
Subject: [PATCH v9] Virtual generated columns

TODO:

 src/backend/rewrite/rewriteHandler.c          | 109 ++-
 src/test/regress/expected/rowsecurity.out     |  29 +
 src/test/regress/sql/collate.icu.utf8.sql     |  15 +
 src/test/regress/sql/rowsecurity.sql          |  27 +
 59 files changed, 1470 insertions(+), 714 deletions(-)

 drop table test8;

 
 
 select * from grem1;
- a  | b  
-----+----
-  1 |  2
- 22 | 44
+ a  | b  | c  
+----+----+----
+  1 |  2 |  3
+ 22 | 44 | 66
 (2 rows)
 
 delete from grem1;
 -- test copy from
 copy grem1 from stdin;
 select * from gloc1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 |  
+ 2 | 4 |  
 (2 rows)
 
 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;

 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;
index c180ed7abbc..b6ee20573e5 100644
 
index 3c56610d2ac..3769afd92d9 100644
 
   <para>
 
   <para>
index 6098ebed433..17aae68af43 100644
@@ -267,6 +267,11 @@ <title>Description</title>
 
@@ -279,10 +284,15 @@ <title>Description</title>
       longer apply the generation expression.
      </para>
 
+     <para>
index fc81ba3c498..7fe13e6e584 100644

@@ -281,7 +281,7 @@ <title>Parameters</title>
    </varlistentry>
 
    <varlistentry>
-    <term><literal>GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED</literal><indexterm><primary>generated column</primary></indexterm></term>
+    <term><literal>GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) [ STORED | VIRTUAL ]</literal><indexterm><primary>generated column</primary></indexterm></term>
     <listitem>
      <para>
       This clause creates the column as a <firstterm>generated
@@ -290,10 +290,13 @@ <title>Parameters</title>
      </para>
 
      <para>
 
      <para>
index dd83b07d65f..47f72896d24 100644
@@ -725,8 +725,9 @@ <title>Parameters</title>
@@ -907,7 +908,7 @@ <title>Parameters</title>
    </varlistentry>
 
@@ -916,8 +917,11 @@ <title>Parameters</title>
      </para>
 
      <para>
 
      <para>
@@ -2450,9 +2454,9 @@ <title>Multiple Identity Columns</title>
    <title>Generated Columns</title>
 
    <para>

    <para>

 
 		if (att->attisdropped)
 			continue;
index 003af4bf21c..b2ef8254235 100644

index d1134733c17..25c1fbf972f 100644
@@ -1111,6 +1111,9 @@ DefineIndex(Oid tableId,
@@ -1120,14 +1123,22 @@ DefineIndex(Oid tableId,
 
 	/*
 
@@ -1140,6 +1151,22 @@ DefineIndex(Oid tableId,

 
 
 			Assert(expr != NULL);

 			k = -1;

 			/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ccd9645e7d2..2907974eed3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3020,6 +3020,15 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 									 errhint("A child table column cannot be generated unless its parent column is.")));
 					}
 
@@ -3292,6 +3301,15 @@ MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const
 
@@ -6093,7 +6111,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
@@ -7271,7 +7289,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,

@@ -7748,6 +7766,14 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
+	/* TODO: see transformColumnDefinition() */
+	if (TupleDescAttr(RelationGetDescr(rel), attnum - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("not-null constraints are not supported on virtual generated columns"),
+				 errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
+						   colName, RelationGetRelationName(rel))));
+
 	/* See if there's already a constraint */
 	tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
 	if (HeapTupleIsValid(tuple))
@@ -8396,7 +8422,18 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
@@ -8553,17 +8590,30 @@ ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMOD
 
@@ -8706,6 +8756,16 @@ ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa
 
+	/*
@@ -9895,6 +9955,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -12051,7 +12124,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 			val = SysCacheGetAttrNotNull(CONSTROID, tuple,
 										 Anum_pg_constraint_conbin);
 			conbin = TextDatumGetCString(val);
-			newcon->qual = (Node *) stringToNode(conbin);
+			newcon->qual = (Node *) expand_generated_columns_in_expr(stringToNode(conbin), rel, 1);
 
 			/* Find or create work queue entry for this table */
 			tab = ATGetQueueEntry(wqueue, rel);
@@ -13233,8 +13306,12 @@ ATPrepAlterColumnType(List **wqueue,
 					   list_make1_oid(rel->rd_rel->reltype),
 					   0);
 
@@ -13307,11 +13384,12 @@ ATPrepAlterColumnType(List **wqueue,
 
@@ -16297,6 +16375,14 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 
@@ -18526,8 +18612,11 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 
 			/*
@@ -18609,9 +18698,12 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 				}
 
 				/*
index 09356e46d16..a9058370348 100644

 /*

 			/*
 
 			/*

 
+		/* Internal error: somebody forgot to expand it. */
+		if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			elog(ERROR, "unexpected virtual generated column reference");
+
 		if (attr->attisdropped)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 5ca856fd279..e49acc20845 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1757,6 +1757,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
 			Expr	   *checkconstr;
 
@@ -2304,7 +2305,9 @@ ExecBuildSlotValueDescription(Oid reloid,
 
index 67eb96396af..242a18cf78d 100644

 
@@ -3985,7 +3985,7 @@ ColConstraintElem:
 					n->location = @1;
 					$$ = (Node *) n;
 				}
-			| GENERATED generated_when AS '(' a_expr ')' STORED
+			| GENERATED generated_when AS '(' a_expr ')' opt_virtual_or_stored
 				{
 					Constraint *n = makeNode(Constraint);
 
@@ -3993,6 +3993,7 @@ ColConstraintElem:
 					n->generated_when = $2;
 					n->raw_expr = $5;
 					n->cooked_expr = NULL;
+					n->generated_kind = $7;
 					n->location = @1;
 
 					/*
@@ -4039,6 +4040,12 @@ generated_when:
 			| BY DEFAULT	{ $$ = ATTRIBUTE_IDENTITY_BY_DEFAULT; }
 		;
 
+opt_virtual_or_stored:
@@ -17921,6 +17928,7 @@ unreserved_keyword:
@@ -18575,6 +18583,7 @@ bare_label_keyword:
 
index 0f324ee4e31..bcfab424c1e 100644
@@ -889,7 +889,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
@@ -986,6 +986,18 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 
 	/*
index a6002b223df..3d11d7c5918 100644
@@ -1000,7 +1001,7 @@ pgoutput_row_filter_init(PGOutputData *data, List *publications,
 				continue;

index 063afd4933e..d59fe2e7ce8 100644
@@ -96,6 +96,7 @@ static List *matchLocks(CmdType event, Relation relation,

 /*
@@ -980,7 +981,8 @@ rewriteTargetListIU(List *targetList,
 		if (att_tup->attgenerated)
 		{
 			/*
-			 * stored generated column will be fixed in executor
+			 * virtual generated column stores a null value; stored generated
+			 * column will be fixed in executor
 			 */
 			new_tle = NULL;
 		}
@@ -2204,6 +2206,10 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 	 * requires special recursion detection if the new quals have sublink
 	 * subqueries, and if we did it in the loop above query_tree_walker would
 	 * then recurse into those quals a second time.
+	 *
+	 * Finally, we expand any virtual generated columns.  We do this after
+	 * each table's RLS policies are applied because the RLS policies might
+	 * also refer to the table's virtual generated columns.
 	 */
 	rt_index = 0;
 	foreach(lc, parsetree->rtable)
@@ -2217,10 +2223,11 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 
 		++rt_index;
 
-		/* Only normal relations can have RLS policies */
-		if (rte->rtekind != RTE_RELATION ||
-			(rte->relkind != RELKIND_RELATION &&
-			 rte->relkind != RELKIND_PARTITIONED_TABLE))
+		/*
+		 * Only normal relations can have RLS policies or virtual generated
+		 * columns.
+		 */
+		if (rte->rtekind != RTE_RELATION)
 			continue;
 
 		rel = table_open(rte->relid, NoLock);
@@ -2309,6 +2316,14 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		if (hasSubLinks)
 			parsetree->hasSubLinks = true;
 
+		/*
+		 * Expand any references to virtual generated columns of this table.
+		 * Note that subqueries in virtual generated column expressions are
+		 * not currently supported, so this cannot add any more sublinks.
+		 */
+		parsetree = (Query *) expand_generated_columns_internal((Node *) parsetree,
+																rel, rt_index, rte);
+
 		table_close(rel, NoLock);
 	}
 
@@ -4420,6 +4435,90 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
 }

+/*
index 342467fd186..60c477ec9f9 100644
 
index a8c141b689d..86cc1dd1339 100644
@@ -16182,6 +16182,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
index aa1564cd450..8fc981d52e9 100644
@@ -3641,12 +3641,14 @@
index 5bfebad64d5..e257863cde4 100644
@@ -2084,6 +2084,12 @@ describeOneTableDetails(const char *schemaname,

 /*
index 8c278f202b4..ec95d9ba7f1 100644

 
 
index 0f9462493e3..07204b7bf62 100644

 

 

 

 

 
 CREATE VIEW trigger_test_view AS SELECT i, v FROM trigger_test;
diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out
index 6fa32ae3649..37fb9afd7aa 100644
--- a/src/test/regress/expected/collate.icu.utf8.out
+++ b/src/test/regress/expected/collate.icu.utf8.out
@@ -2378,6 +2378,26 @@ DROP TABLE pagg_tab6;
 RESET enable_partitionwise_aggregate;
 RESET max_parallel_workers_per_gather;
 RESET enable_incremental_sort;
+-- virtual generated columns
+CREATE TABLE t5 (
+    a int,
+    b text collate "C",
+    c text collate "C" GENERATED ALWAYS AS (b COLLATE case_insensitive)
+);
+INSERT INTO t5 (a, b) values (1, 'D1'), (2, 'D2'), (3, 'd1');
+-- Collation of c should be the one defined for the column ("C"), not
+-- the one of the generation expression.  (Note that we cannot just
+-- test with, say, using COLLATION FOR, because the collation of
+-- function calls is already determined in the parser before
+-- rewriting.)
+SELECT * FROM t5 ORDER BY c ASC, a ASC;
+ a | b  | c  
+---+----+----
+ 1 | D1 | D1
+ 2 | D2 | D2
+ 3 | d1 | d1
+(3 rows)
+
 -- cleanup
 RESET search_path;
 SET client_min_messages TO warning;
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index d091da5a1ef..490ec986b35 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -113,19 +113,20 @@ SELECT * FROM test_like_id_3;  -- identity was copied and applied
 (1 row)

 

 
 
index 0d037d48ca0..1095e945b7a 100644

+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
@@ -764,6 +808,11 @@ CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a *
@@ -794,6 +843,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -807,6 +861,11 @@ ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09
@@ -1094,9 +1153,9 @@ SELECT * FROM gtest29;
@@ -1190,6 +1249,18 @@ Inherits: gtest30
 
index 0d037d48ca0..b66eba63db8 100644

 
 \d gtest1
 
 -- duplicate generated

 
+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
 
 \d gtest1_1

@@ -312,25 +346,44 @@ ERROR:  column "b" inherits from generated column but specifies default
 Not-null constraints:
     "gtest1_a_not_null" NOT NULL "a" (inherited)
 Inherits: gtest1
 
@@ -343,28 +396,28 @@ CREATE TABLE gtest1_y () INHERITS (gtest1, gtesty);  -- error
 
@@ -381,8 +434,8 @@ TABLE gtestc;
 
@@ -403,7 +456,7 @@ SELECT * FROM gtest3 ORDER BY a;
     |   
 (4 rows)
 
@@ -468,7 +521,7 @@ SELECT * FROM gtest3 ORDER BY a;
 (4 rows)
 
 -- null values
-CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED);
+CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) VIRTUAL);
 INSERT INTO gtest2 VALUES (1);
 SELECT * FROM gtest2;
  a | b 
@@ -477,7 +530,7 @@ SELECT * FROM gtest2;
 (1 row)
 
@@ -492,7 +545,7 @@ DROP TABLE gtest_varlena;
@@ -507,11 +560,11 @@ DROP TYPE double_int;
@@ -520,7 +573,7 @@ SELECT * FROM gtest_tableoid;
 (2 rows)
 
 -- drop column behavior
@@ -528,30 +581,31 @@ HINT:  Use DROP ... CASCADE to drop the dependent objects too.
 
@@ -560,231 +614,142 @@ SELECT a, c FROM gtest11s;  -- allowed
 
@@ -794,6 +759,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -808,32 +778,37 @@ ERROR:  table "gtest_child3" being attached contains an identity column "f3"
 
 \d gtest_child2
 
 \d gtest_child3
 
@@ -844,103 +819,127 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;

 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3

 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3
 
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
@@ -953,54 +952,54 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;

 
 \d gtest25
 
@@ -1008,7 +1007,7 @@ Indexes:
 CREATE TABLE gtest27 (
     a int,
     b int,
@@ -1016,12 +1015,12 @@ ERROR:  cannot alter type of a column used by a generated column
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1035,20 +1034,19 @@ ERROR:  cannot specify USING when altering type of generated column
 
@@ -1057,12 +1055,12 @@ ALTER TABLE gtest27
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1074,7 +1072,7 @@ SELECT * FROM gtest27;
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -1085,115 +1083,138 @@ SELECT * FROM gtest29;
 (2 rows)
 
 \d gtest29

 \d gtest29

 \d gtest29

 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
 
 \d gtest30_1

 \d gtest30_1
 
@@ -1246,7 +1267,7 @@ CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26
@@ -1256,12 +1277,12 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
@@ -1271,8 +1292,8 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
@@ -1283,6 +1304,8 @@ SELECT * FROM gtest26 ORDER BY a;
@@ -1296,9 +1319,11 @@ $$;
@@ -1322,14 +1347,13 @@ CREATE TRIGGER gtest12_03 BEFORE UPDATE ON gtest26
 
@@ -1337,22 +1361,22 @@ CREATE TABLE gtest28a (

diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 6d127c19f47..af765edf31c 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -4379,6 +4379,35 @@ ERROR:  new row violates row-level security policy for table "r1"
 INSERT INTO r1 VALUES (10)
     ON CONFLICT ON CONSTRAINT r1_pkey DO UPDATE SET a = 30;
 ERROR:  new row violates row-level security policy for table "r1"
+DROP TABLE r1;
+--
+-- Test policies using virtual generated columns
+--
+SET SESSION AUTHORIZATION regress_rls_alice;
+SET row_security = on;
+CREATE TABLE r1 (a int, b int GENERATED ALWAYS AS (a * 10) VIRTUAL);
+ALTER TABLE r1 ADD c int GENERATED ALWAYS AS (a * 100) VIRTUAL;
+INSERT INTO r1 VALUES (1), (2), (4);
+CREATE POLICY p0 ON r1 USING (b * 10 = c);
+CREATE POLICY p1 ON r1 AS RESTRICTIVE USING (b > 10);
+CREATE POLICY p2 ON r1 AS RESTRICTIVE USING ((SELECT c) < 400);
+ALTER TABLE r1 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r1 FORCE ROW LEVEL SECURITY;
+-- Should fail p1
+INSERT INTO r1 VALUES (0);
+ERROR:  new row violates row-level security policy "p1" for table "r1"
+-- Should fail p2
+INSERT INTO r1 VALUES (4);
+ERROR:  new row violates row-level security policy "p2" for table "r1"
+-- OK
+INSERT INTO r1 VALUES (3);
+SELECT * FROM r1;
+ a | b  |  c  
+---+----+-----
+ 2 | 20 | 200
+ 3 | 30 | 300
+(2 rows)
+
 DROP TABLE r1;
 -- Check dependency handling
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 81e4222d26a..782d7d11dcb 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -68,6 +68,9 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated_stored join_hash
 
+# TODO: find a place for this (above group is full)
+test: generated_virtual
+
 # ----------
 # Additional BRIN tests
 # ----------
diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql
index 49fa9758b40..8b5cd5b38df 100644
--- a/src/test/regress/sql/collate.icu.utf8.sql
+++ b/src/test/regress/sql/collate.icu.utf8.sql
@@ -898,6 +898,21 @@ CREATE TABLE pagg_tab6_p2 PARTITION OF pagg_tab6 FOR VALUES IN ('c', 'd');
 RESET max_parallel_workers_per_gather;
 RESET enable_incremental_sort;
 
index dea8942c71f..63fd897969a 100644

 

+CREATE TABLE gtestm (
+  a int PRIMARY KEY,

 

 

 \d gtest1
 
 -- duplicate generated

 -- invalid reference

 

 

 
 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,

 

 
 
--- test stored update

 -- null values

 -- drop column behavior
 
 \d gtest10

 -- privileges

 
 
 SET ROLE regress_user11;

 -- check constraints

 
 
--- not-null constraints

 -- index constraints
 
 -- indexes
 
 -- foreign keys

 
 
-DROP TABLE gtest23b;
-DROP TABLE gtest23a;
+--DROP TABLE gtest23b;
+--DROP TABLE gtest23a;

 
 -- domains

 -- partitioning cases

 

 

 
 ALTER TABLE gtest28a DROP COLUMN a;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index eab7d99003e..1557f200cf5 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2072,6 +2072,33 @@ CREATE POLICY p3 ON r1 FOR INSERT WITH CHECK (true);
 
 DROP TABLE r1;
 
+--
+-- Test policies using virtual generated columns
+--
+SET SESSION AUTHORIZATION regress_rls_alice;
+SET row_security = on;
+CREATE TABLE r1 (a int, b int GENERATED ALWAYS AS (a * 10) VIRTUAL);
+ALTER TABLE r1 ADD c int GENERATED ALWAYS AS (a * 100) VIRTUAL;
+INSERT INTO r1 VALUES (1), (2), (4);
+
+CREATE POLICY p0 ON r1 USING (b * 10 = c);
+CREATE POLICY p1 ON r1 AS RESTRICTIVE USING (b > 10);
+CREATE POLICY p2 ON r1 AS RESTRICTIVE USING ((SELECT c) < 400);
+ALTER TABLE r1 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r1 FORCE ROW LEVEL SECURITY;
+
+-- Should fail p1
+INSERT INTO r1 VALUES (0);
+
+-- Should fail p2
+INSERT INTO r1 VALUES (4);
+
+-- OK
+INSERT INTO r1 VALUES (3);
+SELECT * FROM r1;
+
+DROP TABLE r1;
+
 -- Check dependency handling
 RESET SESSION AUTHORIZATION;
 CREATE TABLE dep1 (c1 int);
diff --git a/src/test/subscription/t/011_generated.pl b/src/test/subscription/t/011_generated.pl
index 211b54c3162..a721bb573b1 100644
--- a/src/test/subscription/t/011_generated.pl
+++ b/src/test/subscription/t/011_generated.pl
@@ -21,11 +21,11 @@
 my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';

 
 # data for initial sync
@@ -42,10 +42,10 @@

 # data to replicate
 
@@ -56,11 +56,11 @@

 
@@ -69,7 +69,7 @@
 
@@ -88,13 +88,13 @@ BEGIN
 
 $result =
 
 # cleanup
 $node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");

base-commit: db22b900244d3c51918acce44cbe5bb6f6507d32
-- 
2.47.0

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 12 Nov 2024 17:07:14 +0100

> expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc,

Yeah, I put it back that way in v9.

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 12 Nov 2024 17:08:16 +0100

> so we have to error out at DefineDomain.

This appears to be a very old problem independent of this patch.  I'll 
take a look at fixing it.

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 12 Nov 2024 17:09:35 +0100

> part of the partition key.

True.  I have removed this extra code in v9.

>                           errmsg("trigger modified virtual generated

I don't know.  fastgetattr() is supposed to be "fast". ;-)  It's all 
inline functions, so maybe that is actually correct.  I don't have a 
strong opinion either way.

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 12 Nov 2024 17:10:57 +0100

On 11.11.24 06:51, vignesh C wrote:
> The patch needs to be rebased due to a recent commit 14e87ffa5c5.

done in v9

> I

Yes, I just need a find a good place to put it into 
src/test/subscription/t/028_row_filter.pl.  It's very long. ;-)

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 12 Nov 2024 17:17:16 +0100

> we can allow it in the row filter than why not to publish it as well.

Row filters have pretty strong restrictions for what kind of operations 
they can contain.  Applying those restrictions to virtual generated 
columns would probably not make that feature very useful.  (You want to 
use virtual columns for expressions that are too cumbersome to write out 
by hand every time.)

Moreover, we would have to implement some elaborate cross-checks if a 
table gets added to a publication.  How would that work?  "Can't add 
table x to publication because it contains a virtual generated column 
with a non-simple expression"?  With row filters, this is less of a 
problem, because the row filter a property of the publication.

---

From: Alvaro Herrera <[email-masked]>
Date: Tue, 12 Nov 2024 17:50:41 +0100

> I don't know.  fastgetattr() is supposed to be "fast". ;-)  It's all inline
> functions, so maybe that is actually correct.  I don't have a strong opinion
> either way.

I think Jian is right: if you're only interested in the isnull bit, then
heap_attisnull is more appropriate, because it doesn't have to decode
("deform") the tuple before giving you the answer; it knows the answer
by checking just the nulls bitmap.  With fastgetattr you still fetch the
value from the data bytes, even though your function doesn't care about
it.  That's probably even measurable for wide tuples if the generated
attrs are at the end, which sounds common.

Personally I dislike using 0-based loops for attribute numbers, which
are 1-based.  For peace of mind, I'd write this as

   for (AttrNumber i = 1; i <= tupdesc->natts; i++)
   {
       if (TupleDescAttr(tupdesc, i - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
       {
           bool        isnull;

           fastgetattr(tuple, i, tupdesc, &isnull); // heap_attisnull here actually

I'm kind of annoyed that TupleDescAttr() was made to refer to array
indexes rather than attribute numbers, but by the time I realized it had
happened, it was too late.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"El Maquinismo fue proscrito so pena de cosquilleo hasta la muerte"
(Ijon Tichy en Viajes, Stanislaw Lem)

---

From: Amit Kapila <[email-masked]>
Date: Thu, 14 Nov 2024 15:16:05 +0530

>From this paragraph, it sounds like you are saying we can't support
virtual columns in row filters. But the patch already works (not
checked all possible cases). For example,

postgres=# CREATE TABLE gtest1 (a int PRIMARY KEY, b int GENERATED
ALWAYS AS (a * 2) VIRTUAL);
CREATE TABLE
postgres=# create publication pub2 for table gtest1 WHERE (b > 5);
CREATE PUBLICATION

After this, Insert also adheres to this row filter. I haven't tested
it in any further detail but its basic usage in row filters works.

> problem, because the row filter a property of the publication.

Because virtual generated columns work in row filters, so I thought it
could follow the rules for column lists as well. If the virtual column
doesn't adhere to the rules of the row filter then it shouldn't even
work there. My response was based on the theory that the expression
for virtual columns could be computed during logical decoding. So,
let's first clarify that before discussing this point further.

-- 
With Regards,
Amit Kapila.

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 28 Nov 2024 10:35:51 +0100

On 12.11.24 17:08, Peter Eisentraut wrote:
>> (2) ;

Here is a patch.

I'm on the fence about taking out the default case.  It does catch the 
missing enum values, and I suppose if the struct arrives in 
DefineDomain() with a corrupted contype value that is none of the enum 
values, then we'd just do nothing with it.  Maybe go ahead with this, 
but for backpatching leave the default case in place?

From 09f9484f0d2990b4b9be8d2a8b907c134d6d5ee7 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Thu, 28 Nov 2024 10:23:26 +0100
Subject: [PATCH] Fix handling of CREATE DOMAIN with GENERATED constraint
 syntax

Stuff like

    CREATE DOMAIN foo AS int CONSTRAINT cc GENERATED ALWAYS AS (2) STORED

is not supported for domains, but the parser allows it, because it's
the same syntax as for table constraints.  But CreateDomain() did not
explicitly handle all ConstrType values, so the above would get an
internal error like

Fix that by providing a user-facing error message for all ConstrType
values.  Also, remove the switch default case, so future additions to
ConstrType are caught.

Reported-by: Jian He <[email-masked]>
Discussion: https://www.postgresql.org/message-id/CACJufxF8fmM=[email-masked]
---
 src/backend/commands/typecmds.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 859e2191f08..130741e777e 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1011,10 +1011,14 @@ DefineDomain(CreateDomainStmt *stmt)
 						 errmsg("specifying constraint deferrability not supported for domains")));
 				break;
 
-			default:
-				elog(ERROR, "unrecognized constraint subtype: %d",
-					 (int) constr->contype);
+			case CONSTR_GENERATED:
+			case CONSTR_IDENTITY:
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("specifying GENERATED not supported for domains")));
 				break;
+
+				/* no default, to let compiler warn about missing case */
 		}
 	}
 
-- 
2.47.0

---

From: Peter Eisentraut <[email-masked]>
Date: Fri, 29 Nov 2024 10:46:23 +0100

> let's first clarify that before discussing this point further.

Row filter expressions have restrictions that virtual columns do not 
have.  For example, row filter expressions cannot use user-defined 
functions.  If you have a virtual column that uses a user-defined 
function and then you create a row filter using that virtual column, you 
get an error when you create the publication.  (This does not work 
correctly in the posted patches, but it will in v10 that I will post 
shortly.)  This behavior is ok, I think, you get the error when you 
write the faulty expression, and it's straightforward to implement.

Now let's say that we implement what you suggest that we compute virtual 
columns during logical decoding.  Then we presumably need similar 
restrictions, like not allowing user-defined functions.

Firstly, I don't know if that would be such a good restriction.  For row 
filters, that's maybe ok, but for virtual columns, you want to be able 
to write complex and interesting expressions, otherwise you wouldn't 
need a virtual column.

And secondly, we'd then need to implement logic to check that you can't 
add a table with a virtual column with a user-defined function to a 
publication.  This would happen not when you write the expression but 
only later when you operate on the table or publication.  So it's 
already a dubious user experience.

And the number of combinations and scenarios that you'd need to check 
there is immense.  (Not just CREATE PUBLICATION and ALTER PUBLICATION, 
but also CREATE TABLE when a FOR ALL TABLES publication exists, ALTER 
TABLE when new columns are added, new partitions are attached, and so 
on.)  Maybe someone wants to work on that, but that's more than I am 
currently signed up for.  And given the first point, I'm not sure if 
it's even such a useful feature.

I think, for the first iteration of this virtual generated columns 
feature, the publish_generated_columns option should just not apply to 
it.  Whether that means renaming the option or just documenting this is 
something for discussion.

---

From: Peter Eisentraut <[email-masked]>
Date: Fri, 29 Nov 2024 11:13:45 +0100

Here is a new patch version, with several updates.

- Expanded the commit message.

- Added more documentation on storage of virtual columns.

- Added more documentation and tests on security and access privilege 
questions.  (The functionality itself has not changed.)

- Added support for ALTER TABLE ... SET EXPRESSION.

- Added support for virtual columns in trigger column lists.  (For that, 
I renamed ExecInitStoredGenerated() to ExecInitGenerated(), which 
handles the computation of ri_extraUpdatedCols.)

- Correctly prevent not-null constraints on virtual columns in all cases 
(see nearby message).

- Expand virtual columns when checking publication row filter 
expressions (see nearby message).  Also added tests in 028_row_filter.pl.

According to my notes, this is now feature complete and has no glaring 
unsolved problems known to me.
From b7e8a67e963af585a037ec9a586267f215139585 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Fri, 29 Nov 2024 09:45:32 +0100
Subject: [PATCH v10] Virtual generated columns

The syntax for the column definition is

    ... GENERATED ALWAYS AS (...) VIRTUAL

and VIRTUAL is also optional.  VIRTUAL is the default rather than
STORED to match various other SQL products.  (The SQL standard makes
no specification about this, but it also doesn't know about VIRTUAL or
STORED.)  (Also, virtual views are the default, rather than
materialized views.)

Virtual generated columns are stored in tuples as null values.  (A
very early version of this patch had the ambition to not store them at
all.  But so much stuff breaks or gets confused if you have tuples
where a column in the middle is completely missing.  This is a
compromise, and it still saves space over being forced to use stored
generated columns.  If we ever find a way to improve this, a bit of
pg_upgrade cleverness could allow for upgrades to a newer scheme.)

The capabilities and restrictions of virtual generated columns are
mostly the same as for stored generated columns.  In some cases, this
patch keeps virtual generated columns more restricted than they might
technically need to be, to keep the two kinds consistent.  Some of
that could maybe be relaxed later after separate careful
considerations.

Some functionality that is currently not supported, but could possibly
be added as incremental features, some easier than others:

- index on or using a virtual column
- hence also no unique constraints on virtual columns
- extended statistics on virtual columns
- foreign key constraints on virtual columns
- not-null constraints on virtual columns (check constraints are supported)
- ALTER TABLE / DROP EXPRESSION
- virtual column cannot have domain type

The tests in generated_virtual.sql have been copied over from
generated_stored.sql with the keyword replaced.  This way we can make
sure the behavior is mostly aligned, and the differences can be
visible.  Some tests for currently not supported features are
currently commented out.

TODO:
- handling of publication option publish_generated_columns

Discussion: https://www.postgresql.org/message-id/flat/[email-masked]
---
 contrib/pageinspect/expected/page.out         |  37 +
 contrib/pageinspect/sql/page.sql              |  19 +
 .../postgres_fdw/expected/postgres_fdw.out    |  81 +-
 contrib/postgres_fdw/sql/postgres_fdw.sql     |   9 +-
 doc/src/sgml/catalogs.sgml                    |   6 +-
 doc/src/sgml/ddl.sgml                         |  45 +-
 doc/src/sgml/ref/alter_table.sgml             |  11 +-
 doc/src/sgml/ref/create_foreign_table.sgml    |  11 +-
 doc/src/sgml/ref/create_table.sgml            |  22 +-
 doc/src/sgml/trigger.sgml                     |   4 +
 src/backend/access/common/tupdesc.c           |   3 +
 src/backend/access/heap/heapam_handler.c      |   2 +
 src/backend/catalog/heap.c                    |  23 +-
 src/backend/commands/analyze.c                |   4 +
 src/backend/commands/indexcmds.c              |  33 +-
 src/backend/commands/publicationcmds.c        |   3 +
 src/backend/commands/statscmds.c              |  20 +-
 src/backend/commands/tablecmds.c              | 196 +++-
 src/backend/commands/trigger.c                |  44 +-
 src/backend/executor/execExprInterp.c         |   4 +
 src/backend/executor/execMain.c               |   5 +-
 src/backend/executor/execUtils.c              |   4 +-
 src/backend/executor/nodeModifyTable.c        |  25 +-
 src/backend/parser/gram.y                     |  15 +-
 src/backend/parser/parse_relation.c           |   6 +-
 src/backend/parser/parse_utilcmd.c            |  16 +-
 src/backend/replication/pgoutput/pgoutput.c   |   3 +-
 src/backend/rewrite/rewriteHandler.c          | 115 ++-
 src/backend/utils/cache/relcache.c            |   3 +
 src/bin/pg_dump/pg_dump.c                     |   3 +
 src/bin/pg_dump/t/002_pg_dump.pl              |   6 +-
 src/bin/psql/describe.c                       |   6 +
 src/include/access/tupdesc.h                  |   1 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/pg_attribute.h            |   1 +
 src/include/executor/nodeModifyTable.h        |   6 +-
 src/include/nodes/execnodes.h                 |   3 +
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/rewrite/rewriteHandler.h          |   2 +
 src/pl/plperl/expected/plperl_trigger.out     |   7 +-
 src/pl/plperl/plperl.c                        |   3 +
 src/pl/plperl/sql/plperl_trigger.sql          |   3 +-
 src/pl/plpython/expected/plpython_trigger.out |   7 +-
 src/pl/plpython/plpy_typeio.c                 |   3 +
 src/pl/plpython/sql/plpython_trigger.sql      |   3 +-
 src/pl/tcl/expected/pltcl_trigger.out         |  19 +-
 src/pl/tcl/pltcl.c                            |   3 +
 src/pl/tcl/sql/pltcl_trigger.sql              |   3 +-
 .../regress/expected/collate.icu.utf8.out     |  20 +
 .../regress/expected/create_table_like.out    |  23 +-
 src/test/regress/expected/fast_default.out    |  12 +
 .../regress/expected/generated_stored.out     |  83 +-
 ...rated_stored.out => generated_virtual.out} | 835 +++++++++---------
 src/test/regress/expected/publication.out     |  18 +-
 src/test/regress/expected/rowsecurity.out     |  29 +
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/sql/collate.icu.utf8.sql     |  15 +
 src/test/regress/sql/create_table_like.sql    |   2 +-
 src/test/regress/sql/fast_default.sql         |  11 +
 src/test/regress/sql/generated_stored.sql     |  43 +-
 ...rated_stored.sql => generated_virtual.sql} | 321 ++++---
 src/test/regress/sql/publication.sql          |  14 +-
 src/test/regress/sql/rowsecurity.sql          |  27 +
 src/test/subscription/t/011_generated.pl      |  39 +-
 src/test/subscription/t/028_row_filter.pl     |  38 +-
 66 files changed, 1642 insertions(+), 742 deletions(-)
 copy src/test/regress/expected/{generated_stored.out => generated_virtual.out} (68%)
 copy src/test/regress/sql/{generated_stored.sql => generated_virtual.sql} (68%)

 drop table test8;

 
 
 select * from grem1;
- a  | b  
-----+----
-  1 |  2
- 22 | 44
+ a  | b  | c  
+----+----+----
+  1 |  2 |  3
+ 22 | 44 | 66
 (2 rows)
 
 delete from grem1;
 -- test copy from
 copy grem1 from stdin;
 select * from gloc1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 |  
+ 2 | 4 |  
 (2 rows)
 
 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;

 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;
index 59bb833f48d..efcf12a596a 100644
@@ -1318,8 +1318,10 @@ <title><structname>pg_attribute</structname> Columns</title>
+       virtual.  A stored generated column is physically stored like a normal
+       column.  A virtual generated column is physically stored as a null
+       value, with the actual value being computed at run time.
       </para></entry>
      </row>
 
index 08155b156a5..b0ed16a9d30 100644
 
   <para>
 
   <para>
@@ -442,12 +441,18 @@ <title>Generated Columns</title>
       <listitem>
        <para>
         If a parent column is a generated column, its child column must also
-        be a generated column; however, the child column can have a
-        different generation expression.  The generation expression that is
+        be a generated column of the same kind (stored or virtual); however,
+        the child column can have a different generation expression.
+       </para>
+
+       <para>
+        For stored generated columns, the generation expression that is
         actually applied during insert or update of a row is the one
-        associated with the table that the row is physically in.
-        (This is unlike the behavior for column defaults: for those, the
-        default value associated with the table named in the query applies.)
+        associated with the table that the row is physically in.  (This is
+        unlike the behavior for column defaults: for those, the default value
+        associated with the table named in the query applies.)  For virtual
+        generated columns, the generation expression of the table named in the
+        query applies when a table is read.
        </para>
       </listitem>
       <listitem>
@@ -502,6 +507,26 @@ <title>Generated Columns</title>
       particular role can read from a generated column but not from the
       underlying base columns.
      </para>
+
+     <para>
+      For virtual generated columns, this is only fully secure if the
+      generation expression uses only leakproof functions (see <xref
+      linkend="sql-createfunction"/>), but this is not enforced by the system.
+     </para>
+    </listitem>
+    <listitem>
+     <para>
+      Privileges of functions used in generation expressions are checked when
+      the expression is actually executed, on write or read respectively, as
+      if the generation expression had been called directly from the query
+      using the generated column.  The user of a generated column must have
+      permissions to call all functions used by the generation expression.
+      Functions in the generation expression are executed with the privileges
+      of the user executing the query or the function owner, depending on
+      whether the functions are defined as <literal>SECURITY INVOKER</literal>
+      or <literal>SECURITY DEFINER</literal>.
+      <!-- matches create_view.sgml -->
+     </para>
     </listitem>
     <listitem>
      <para>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index c8f7ab7d956..3c1e1b77beb 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -102,7 +102,7 @@
   NULL |
   CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
   DEFAULT <replaceable>default_expr</replaceable> |
-  GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED |
+  GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) [ STORED | VIRTUAL ] |
   GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( <replaceable>sequence_options</replaceable> ) ] |
   UNIQUE [ NULLS [ NOT ] DISTINCT ] <replaceable class="parameter">index_parameters</replaceable> |
   PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
@@ -279,10 +279,15 @@ <title>Description</title>
       longer apply the generation expression.
      </para>
 
+     <para>

 
    <varlistentry>
 
      <para>
 
      <para>
index 1a1adc5ae87..5b7d428e2df 100644

      <para>
 
      <para>
@@ -2457,9 +2461,9 @@ <title>Multiple Identity Columns</title>
    <title>Generated Columns</title>
 
    <para>

    <para>

 
 		if (att->attisdropped)
 			continue;
index d7b88b61dcc..165ebae9abd 100644
 
@@ -587,6 +587,17 @@ CheckAttributeType(const char *attname,
 	}
 	else if (att_typtype == TYPTYPE_DOMAIN)
 	{
+		/*
+		 * Prevent virtual generated columns from having a domain type.  We
+		 * would have to enforce domain constraints when columns underlying
+		 * the generated column change.  This could possibly be implemented,
+		 * but it's not.
+		 */
+		if (flags & CHKATYPE_IS_VIRTUAL)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("virtual generated column \"%s\" cannot have a domain type", attname)));
+
 		/*
 		 * If it's a domain, recurse to check its base type.
 		 */
@@ -2560,6 +2571,11 @@ AddRelationNewConstraints(Relation rel,
 						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						errmsg("cannot add not-null constraint on system column \"%s\"",
 							   strVal(linitial(cdef->keys))));
+			/* TODO: see transformColumnDefinition() */
+			if (get_attgenerated(RelationGetRelid(rel), colnum) == ATTRIBUTE_GENERATED_VIRTUAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("not-null constraints are not supported on virtual generated columns")));
 
 			/*
 			 * If the column already has a not-null constraint, we don't want
@@ -2848,6 +2864,11 @@ AddRelationNotNullConstraints(Relation rel, List *constraints,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot add not-null constraint on system column \"%s\"",
 						   strVal(linitial(constr->keys))));
+		/* TODO: see transformColumnDefinition() */
+		if (get_attgenerated(RelationGetRelid(rel), attnum) == ATTRIBUTE_GENERATED_VIRTUAL)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("not-null constraints are not supported on virtual generated columns")));
 
 		/*
 		 * A column can only have one not-null constraint, so discard any
 
index 4049ce1a10f..becbd556e48 100644
@@ -1120,14 +1123,24 @@ DefineIndex(Oid tableId,
+					 stmt->isconstraint ?
+					 errmsg("unique constraints on virtual generated columns are not supported") :
+					 errmsg("indexes on virtual generated columns are not supported")));
 	}
 
 	/*
 
 		pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs);
 		pull_varattnos((Node *) indexInfo->ii_Predicate, 1, &indexattrs);
@@ -1140,6 +1153,24 @@ DefineIndex(Oid tableId,
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("index creation on system columns is not supported")));
 		}
+
+		/*
+		 * XXX Virtual generated columns in index expressions or predicates
+		 * could be supported, but it needs support in
+		 * RelationGetIndexExpressions() and RelationGetIndexPredicate().
+		 */
+		j = -1;
+		while ((j = bms_next_member(indexattrs, j)) >= 0)
+		{
+			AttrNumber	attno = j + FirstLowInvalidHeapAttributeNumber;
+
+			if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 stmt->isconstraint ?
+						 errmsg("unique constraints on virtual generated columns are not supported") :
+						 errmsg("indexes on virtual generated columns are not supported")));
+		}
 	}
 
 	/* Is index safe for others to ignore?  See set_indexsafe_procflags() */
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 031c84ec29f..3dc873fdc07 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -38,6 +38,7 @@
 #include "parser/parse_clause.h"
 #include "parser/parse_collate.h"
 #include "parser/parse_relation.h"
+#include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
@@ -661,6 +662,8 @@ TransformPubWhereClauses(List *tables, const char *queryString,
 		/* Fix up collation information */
 		assign_expr_collations(pstate, whereclause);
 
+		whereclause = (Node *) expand_generated_columns_in_expr(whereclause, pri->relation, 1);
+
 		/*
 		 * We allow only simple expressions in row filters. See
 		 * check_simple_rowfilter_expr_walker.
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index 1db3ef69d22..744b3508c24 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -246,6 +246,12 @@ CreateStatistics(CreateStatsStmt *stmt)
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("statistics creation on system columns is not supported")));

 
 			Assert(expr != NULL);

 			k = -1;

 			/*
index 6ccae4cb4a8..28623672fa0 100644

 

+	/* TODO: see transformColumnDefinition() */
+	if (TupleDescAttr(RelationGetDescr(rel), attnum - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("not-null constraints are not supported on virtual generated columns"),
+				 errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
+						   colName, RelationGetRelationName(rel))));
+
 	/* See if there's already a constraint */
 	tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
 	if (HeapTupleIsValid(tuple))
@@ -8374,6 +8400,8 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	HeapTuple	tuple;
 	Form_pg_attribute attTup;
 	AttrNumber	attnum;
+	char		attgenerated;
+	bool		rewrite;
 	Oid			attrdefoid;
 	ObjectAddress address;
 	Expr	   *defval;
@@ -8388,36 +8416,70 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 						colName, RelationGetRelationName(rel))));
 
 	attTup = (Form_pg_attribute) GETSTRUCT(tuple);
-	attnum = attTup->attnum;
 
+	attnum = attTup->attnum;
 	if (attnum <= 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
-	if (attTup->attgenerated != ATTRIBUTE_GENERATED_STORED)
+	attgenerated = attTup->attgenerated;
+	if (!attgenerated)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("column \"%s\" of relation \"%s\" is not a generated column",
 						colName, RelationGetRelationName(rel))));
-	ReleaseSysCache(tuple);
 
 	/*
-	 * Clear all the missing values if we're rewriting the table, since this
-	 * renders them pointless.
+	 * TODO: This could be done, just need to recheck any constraints
+	 * afterwards.
 	 */
-	RelationClearMissing(rel);
-
-	/* make sure we don't conflict with later attribute modifications */
-	CommandCounterIncrement();
+	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
+		rel->rd_att->constr && rel->rd_att->constr->num_check > 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables with check constraints"),
+				 errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
+						   colName, RelationGetRelationName(rel))));
 
 	/*
-	 * Find everything that depends on the column (constraints, indexes, etc),
-	 * and record enough information to let us recreate the objects after
-	 * rewrite.
+	 * We need to prevent this because a change of expression could affect a
+	 * row filter and inject expressions that are not permitted in a row
+	 * filter.  XXX We could try to have a more precise check to catch only
+	 * publications with row filters, or even re-verify the row filter
+	 * expressions.
 	 */
-	RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName);
+	if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL &&
+		GetRelationPublications(RelationGetRelid(rel)) != NIL)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables that are part of a publication"),
+				 errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.",
+						   colName, RelationGetRelationName(rel))));
+
+	rewrite = (attgenerated == ATTRIBUTE_GENERATED_STORED);
+
+	ReleaseSysCache(tuple);
+
+	if (rewrite)
+	{
+		/*
+		 * Clear all the missing values if we're rewriting the table, since
+		 * this renders them pointless.
+		 */
+		RelationClearMissing(rel);
+
+		/* make sure we don't conflict with later attribute modifications */
+		CommandCounterIncrement();
+
+		/*
+		 * Find everything that depends on the column (constraints, indexes,
+		 * etc), and record enough information to let us recreate the objects
+		 * after rewrite.
+		 */
+		RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName);
+	}
 
 	/*
 	 * Drop the dependency records of the GENERATED expression, in particular
@@ -8446,7 +8508,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	rawEnt->attnum = attnum;
 	rawEnt->raw_default = newExpr;
 	rawEnt->missingMode = false;
-	rawEnt->generated = ATTRIBUTE_GENERATED_STORED;
+	rawEnt->generated = attgenerated;
 
 	/* Store the generated expression */
 	AddRelationNewConstraints(rel, list_make1(rawEnt), NIL,
@@ -8455,16 +8517,19 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	/* Make above new expression visible */
 	CommandCounterIncrement();
 
-	/* Prepare for table rewrite */
-	defval = (Expr *) build_column_default(rel, attnum);
+	if (rewrite)
+	{
+		/* Prepare for table rewrite */
+		defval = (Expr *) build_column_default(rel, attnum);
 
-	newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue));
-	newval->attnum = attnum;
-	newval->expr = expression_planner(defval);
-	newval->is_generated = true;
+		newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue));
+		newval->attnum = attnum;
+		newval->expr = expression_planner(defval);
+		newval->is_generated = true;
 
-	tab->newvals = lappend(tab->newvals, newval);
-	tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+		tab->newvals = lappend(tab->newvals, newval);
+		tab->rewrite |= AT_REWRITE_DEFAULT_VAL;
+	}
 
 	/* Drop any pg_statistic entry for the column */
 	RemoveStatistics(RelationGetRelid(rel), attnum);
@@ -8553,17 +8618,30 @@ ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMOD
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
@@ -8706,6 +8784,16 @@ ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa
 
+	/*
@@ -9898,6 +9986,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -12099,7 +12200,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 
 			/* Find or create work queue entry for this table */
 			tab = ATGetQueueEntry(wqueue, rel);
@@ -13285,8 +13386,12 @@ ATPrepAlterColumnType(List **wqueue,
 					   list_make1_oid(rel->rd_rel->reltype),
 					   0);
 
@@ -13359,11 +13464,12 @@ ATPrepAlterColumnType(List **wqueue,
 
@@ -16349,6 +16455,14 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 
@@ -18584,8 +18698,11 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 
 			/*
@@ -18667,9 +18784,12 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 				}
 
 				/*
index 09356e46d16..473541a2d21 100644

 /*
@@ -2503,6 +2506,8 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
 
 			/*
@@ -3060,6 +3065,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
 		}
 		else if (newtuple != oldtuple)
 		{
+			check_modified_virtual_generated(RelationGetDescr(relinfo->ri_RelationDesc), newtuple);
+
 			ExecForceStoreHeapTuple(newtuple, newslot, false);
 
 			/*
@@ -3490,6 +3497,8 @@ TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
 
 			oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
 			tgqual = stringToNode(trigger->tgqual);
+			tgqual = (Node *) expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc, PRS2_OLD_VARNO);
+			tgqual = (Node *) expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc, PRS2_NEW_VARNO);
 			/* Change references to OLD and NEW to INNER_VAR and OUTER_VAR */
 			ChangeVarNodes(tgqual, PRS2_OLD_VARNO, INNER_VAR, 0);
 			ChangeVarNodes(tgqual, PRS2_NEW_VARNO, OUTER_VAR, 0);
@@ -6600,3 +6609,36 @@ pg_trigger_depth(PG_FUNCTION_ARGS)
 {
 	PG_RETURN_INT32(MyTriggerDepth);
 }
+
+/*
+ * Check whether a trigger modified a virtual generated column and error if
+ * so.
+ *
+ * We need to check this so that we don't end up storing a non-null value in a
+ * virtual generated column.
+ *
+ * We don't need to check for stored generated columns, since those will be
+ * overwritten later anyway.
+ *
+ * Alternatively, we could fix erroneous tuples here and be silent about it.
+ * This would yield the same user-facing behavior for virtual and stored
+ * generated columns.  But it seems more complicated and not very useful in
+ * practice.
+ */
+static void
+check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple tuple)
+{
+	if (!(tupdesc->constr && tupdesc->constr->has_generated_virtual))
+		return;
+
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+		{
+			if (!heap_attisnull(tuple, i + 1, tupdesc))
+				ereport(ERROR,
+						(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+						 errmsg("trigger modified virtual generated column value")));
+		}
+	}
+}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index bad7b195bfb..e36488a4aa0 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -2133,6 +2133,10 @@ CheckVarSlotCompatibility(TupleTableSlot *slot, int attnum, Oid vartype)

 
 
 		if (table_perm || column_perm)
 		{
-			if (slot->tts_isnull[i])
+			if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+				val = "virtual";
+			else if (slot->tts_isnull[i])
 				val = "null";
 			else
 			{
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index 740e8fb1486..5e1bac04ee3 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -1311,8 +1311,8 @@ Bitmapset *
 ExecGetExtraUpdatedCols(ResultRelInfo *relinfo, EState *estate)
 {
 	/* Compute the info if we didn't already */
-	if (relinfo->ri_GeneratedExprsU == NULL)
-		ExecInitStoredGenerated(relinfo, estate, CMD_UPDATE);
+	if (!relinfo->ri_Generated_valid)
+		ExecInitGenerated(relinfo, estate, CMD_UPDATE);
 	return relinfo->ri_extraUpdatedCols;
 }
 
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 1161520f76b..fc488231f32 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -349,9 +349,9 @@ ExecCheckTIDVisible(EState *estate,
  * UPDATE and INSERT actions.
  */
 void
-ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
-						EState *estate,
-						CmdType cmdtype)
+ExecInitGenerated(ResultRelInfo *resultRelInfo,
+				  EState *estate,
+				  CmdType cmdtype)
 {
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -362,7 +362,7 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
 	MemoryContext oldContext;
 
 	/* Nothing to do if no generated columns */
-	if (!(tupdesc->constr && tupdesc->constr->has_generated_stored))
+	if (!(tupdesc->constr && (tupdesc->constr->has_generated_stored || tupdesc->constr->has_generated_virtual)))
 		return;
 
 	/*
@@ -388,7 +388,9 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
 
 	for (int i = 0; i < natts; i++)
 	{
-		if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_STORED)
+		char		attgenerated = TupleDescAttr(tupdesc, i)->attgenerated;
+
+		if (attgenerated)
 		{
 			Expr	   *expr;
 
@@ -413,8 +415,11 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
 			}
 
 			/* No luck, so prepare the expression for execution */
-			ri_GeneratedExprs[i] = ExecPrepareExpr(expr, estate);
-			ri_NumGeneratedNeeded++;
+			if (attgenerated == ATTRIBUTE_GENERATED_STORED)
+			{
+				ri_GeneratedExprs[i] = ExecPrepareExpr(expr, estate);
+				ri_NumGeneratedNeeded++;
+			}
 
 			/* If UPDATE, mark column in resultRelInfo->ri_extraUpdatedCols */
 			if (cmdtype == CMD_UPDATE)
@@ -442,6 +447,8 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
 		resultRelInfo->ri_NumGeneratedNeededI = ri_NumGeneratedNeeded;
 	}
 
+	resultRelInfo->ri_Generated_valid = true;
+
 	MemoryContextSwitchTo(oldContext);
 }
 
@@ -472,7 +479,7 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
 	if (cmdtype == CMD_UPDATE)
 	{
 		if (resultRelInfo->ri_GeneratedExprsU == NULL)
-			ExecInitStoredGenerated(resultRelInfo, estate, cmdtype);
+			ExecInitGenerated(resultRelInfo, estate, cmdtype);
 		if (resultRelInfo->ri_NumGeneratedNeededU == 0)
 			return;
 		ri_GeneratedExprs = resultRelInfo->ri_GeneratedExprsU;
@@ -480,7 +487,7 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
 	else
 	{
 		if (resultRelInfo->ri_GeneratedExprsI == NULL)
-			ExecInitStoredGenerated(resultRelInfo, estate, cmdtype);
+			ExecInitGenerated(resultRelInfo, estate, cmdtype);
 		/* Early exit is impossible given the prior Assert */
 		Assert(resultRelInfo->ri_NumGeneratedNeededI > 0);
 		ri_GeneratedExprs = resultRelInfo->ri_GeneratedExprsI;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 67eb96396af..242a18cf78d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -628,7 +628,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <str>		opt_existing_window_name
 %type <boolean> opt_if_not_exists
 %type <boolean> opt_unique_null_treatment
-%type <ival>	generated_when override_kind
+%type <ival>	generated_when override_kind opt_virtual_or_stored
 %type <partspec>	PartitionSpec OptPartitionSpec
 %type <partelem>	part_elem
 %type <list>		part_params
@@ -776,7 +776,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	UNLISTEN UNLOGGED UNTIL UPDATE USER USING

 

 					/*
 
+opt_virtual_or_stored:
 
index 0f324ee4e31..929ed4fa806 100644
@@ -986,6 +986,20 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
+					 parser_errposition(cxt->pstate,
+										constraint->location)));
 	}
 
 	/*
index 5e23453f071..d3de5854529 100644

index ab2e2cd6476..889b9156d5e 100644

 /*
@@ -2203,6 +2205,10 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
@@ -2216,10 +2222,11 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 
 		++rt_index;

 		rel = table_open(rte->relid, NoLock);
@@ -2308,6 +2315,14 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		if (hasSubLinks)
 			parsetree->hasSubLinks = true;
 
+		/*
 
@@ -4419,6 +4434,96 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
 }

+/*
+ * Expand virtual generated columns in an expression
+ * This is for expressions that are not part of a query, such as default
+ * expressions or index predicates.  The rt_index is usually 1.
index d0892cee24d..668924e9ceb 100644
 
index add7f16c902..23e85bdcbef 100644
@@ -16188,6 +16188,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
index 0aa39906a11..80dec7291a9 100644
@@ -2106,6 +2106,12 @@ describeOneTableDetails(const char *schemaname,

 /*

 
 
diff --git a/src/include/executor/nodeModifyTable.h b/src/include/executor/nodeModifyTable.h
index 681cdaa89db..0d8a5f66131 100644
--- a/src/include/executor/nodeModifyTable.h
+++ b/src/include/executor/nodeModifyTable.h
@@ -15,9 +15,9 @@
 
 #include "nodes/execnodes.h"
 
-extern void ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
-									EState *estate,
-									CmdType cmdtype);
+extern void ExecInitGenerated(ResultRelInfo *resultRelInfo,
+							  EState *estate,
+							  CmdType cmdtype);
 
 extern void ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
 									   EState *estate, TupleTableSlot *slot,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 182a6956bb0..5acf3c29dec 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -540,6 +540,9 @@ typedef struct ResultRelInfo
 	int			ri_NumGeneratedNeededI;
 	int			ri_NumGeneratedNeededU;
 
+	/* true if the above have been computed */
+	bool		ri_Generated_valid;
+
 	/* list of RETURNING expressions */
 	List	   *ri_returningList;

 

 

 

 
 
index e2d9246a678..a50bd1cd1ea 100644

 
index 79e76d4b850..472407babd6 100644
@@ -2467,6 +2467,26 @@ DROP TABLE pagg_tab6;

 

 
 
index 0d037d48ca0..4374bf55d23 100644

+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
+  b int GENERATED ALWAYS AS (a * 2) STORED
+);
+INSERT INTO gtestm (a) SELECT g FROM generate_series(1, 10) g;
+MERGE INTO gtestm t USING gtestm AS s ON 2 * t.a = s.b WHEN MATCHED THEN DELETE RETURNING *;
+ a  | b  | a  | b  
+----+----+----+----
+  1 |  2 |  1 |  2
+  2 |  4 |  2 |  4
+  3 |  6 |  3 |  6
+  4 |  8 |  4 |  8
+  5 | 10 |  5 | 10
+  6 | 12 |  6 | 12
+  7 | 14 |  7 | 14
+  8 | 16 |  8 | 16
+  9 | 18 |  9 | 18
+ 10 | 20 | 10 | 20
+(10 rows)
+
 DROP TABLE gtestm;
 -- views
 CREATE VIEW gtest1v AS SELECT * FROM gtest1;
@@ -312,6 +352,10 @@ ERROR:  column "b" inherits from generated column but specifies default
 CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS IDENTITY) INHERITS (gtest1);  -- error
 NOTICE:  merging column "b" with inherited definition
 ERROR:  column "b" inherits from generated column but specifies identity
+CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) VIRTUAL) INHERITS (gtest1);  -- error
+NOTICE:  merging column "b" with inherited definition
+ERROR:  column "b" inherits from generated column of different kind
+DETAIL:  Parent column is STORED, child column is VIRTUAL.
 CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) STORED) INHERITS (gtest1);  -- ok, overrides parent
 NOTICE:  merging column "b" with inherited definition
 \d+ gtestx
@@ -547,7 +591,7 @@ CREATE FUNCTION gf1(a int) RETURNS int AS $$ SELECT a * 3 $$ IMMUTABLE LANGUAGE
 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;
+GRANT SELECT (a, c), INSERT ON gtest12s TO regress_user11;
 SET ROLE regress_user11;
 SELECT a, b FROM gtest11s;  -- not allowed
 ERROR:  permission denied for table gtest11s
@@ -560,7 +604,9 @@ SELECT a, c FROM gtest11s;  -- allowed
 
+INSERT INTO gtest12s VALUES (3, 30), (4, 40);  -- currently not allowed because of function permissions, should arguably be allowed
+ERROR:  permission denied for function gf1
+SELECT a, c FROM gtest12s;  -- allowed (does not actually invoke the function)
  1 | 30
@@ -764,6 +810,11 @@ CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a *
@@ -794,6 +845,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -807,6 +863,11 @@ ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09
@@ -1094,9 +1155,9 @@ SELECT * FROM gtest29;
@@ -1190,6 +1251,18 @@ Inherits: gtest30
 
similarity index 68%
index 0d037d48ca0..042e184ea0e 100644

 
 \d gtest1
 
 -- duplicate generated

 
+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
 
 \d gtest1_1

 

 
 
 -- null values

 -- drop column behavior
@@ -528,30 +581,30 @@ HINT:  Use DROP ... CASCADE to drop the dependent objects too.
 
-GRANT SELECT (a, c) ON gtest12s TO regress_user11;
+GRANT SELECT (a, c), INSERT ON gtest12v TO regress_user11;
@@ -560,231 +613,152 @@ SELECT a, c FROM gtest11s;  -- allowed
 
+INSERT INTO gtest12v VALUES (3, 30), (4, 40);  -- allowed (does not actually invoke the function)
+SELECT a, c FROM gtest12v;  -- currently not allowed because of function permissions, should arguably be allowed
+ERROR:  permission denied for function gf1
+ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables with check constraints
+ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables with check constraints
+LINE 1: ... b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL);
+                                                             ^
+-- also check with table constraint syntax
+CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL, CONSTRAINT cc NOT NULL b);  -- error
+CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
+ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;  -- error
+DROP TABLE gtest21ax;
+ERROR:  unique constraints on virtual generated columns are not supported
@@ -794,6 +768,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -808,32 +787,37 @@ ERROR:  table "gtest_child3" being attached contains an identity column "f3"
 
 \d gtest_child2
 
 \d gtest_child3
 
@@ -844,103 +828,121 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;

+ f3     | bigint |           |          | generated always as (f2 * 4)
 
 \d gtest_child
+ f3     | bigint |           |          | generated always as (f2 * 10)
 
 \d gtest_child2
 
 \d gtest_child3
 
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
    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
+ gtest_child  | 07-15-2016 |  2 |  8
+ gtest_child2 | 08-15-2016 |  3 | 12
+ gtest_child3 | 09-13-2016 |  1 |  4
 (3 rows)

 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3
 
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
@@ -953,20 +955,20 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
 
@@ -974,16 +976,16 @@ SELECT * FROM gtest25 ORDER BY a;
  4 | 12
 (2 rows)
 
  a | b  | c  |  x  |  d  |  y  
 ---+----+----+-----+-----+-----
@@ -992,15 +994,15 @@ SELECT * FROM gtest25 ORDER BY a;
 (2 rows)
 
 \d gtest25
+ b      | integer          |           |          | generated always as (a * 3)
 
@@ -1008,7 +1010,7 @@ Indexes:
 CREATE TABLE gtest27 (
     a int,
     b int,
@@ -1016,12 +1018,12 @@ ERROR:  cannot alter type of a column used by a generated column
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1035,20 +1037,19 @@ ERROR:  cannot specify USING when altering type of generated column
 
@@ -1057,12 +1058,12 @@ ALTER TABLE gtest27
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1074,7 +1075,7 @@ SELECT * FROM gtest27;
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -1085,18 +1086,18 @@ SELECT * FROM gtest29;
 (2 rows)
 
 \d gtest29
 
@@ -1107,93 +1108,114 @@ SELECT * FROM gtest29;
 (2 rows)
 
 \d gtest29
+ b      | integer |           |          | generated always as (a * 3)
 
  3 |  9
  4 | 12
- 5 |   
- 6 | 66
-(4 rows)
+ 5 | 15
+(3 rows)
 
 \d gtest29
+ b      | integer |           |          | generated always as (a * 3)
 
+ b      | integer |           |          | generated always as (a * 3)
 
 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
 
 \d gtest30_1

 \d gtest30_1
 
@@ -1246,7 +1268,7 @@ CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26
@@ -1256,12 +1278,12 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
@@ -1271,8 +1293,8 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
 DELETE FROM gtest26 WHERE a = -6;
-INFO:  gtest1: BEFORE: old = (-6,-12)
-INFO:  gtest3: AFTER: old = (-6,-12)
+INFO:  gtest1: BEFORE: old = (-6,)
+INFO:  gtest3: AFTER: old = (-6,)
 SELECT * FROM gtest26 ORDER BY a;
  a | b 
 ---+---
@@ -1300,7 +1322,7 @@ 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
+-- check that modifications of generated columns in triggers do
 -- not get propagated
 CREATE FUNCTION gtest_trigger_func4() RETURNS trigger
   LANGUAGE plpgsql
@@ -1322,14 +1344,13 @@ CREATE TRIGGER gtest12_03 BEFORE UPDATE ON gtest26
   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: old = (1,)
 INFO:  gtest12_01: BEFORE: new = (11,)
-INFO:  gtest12_03: BEFORE: old = (1,2)
-INFO:  gtest12_03: BEFORE: new = (10,)
+ERROR:  trigger modified virtual generated column value
 SELECT * FROM gtest26 ORDER BY a;
- a  | b  
-----+----
- 10 | 20
+ a | b 
+---+---
+ 1 | 2
 (1 row)
 
@@ -1337,22 +1358,22 @@ CREATE TABLE gtest28a (

diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 5de2d64d01a..3b3c6999d6c 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -442,7 +442,7 @@ LINE 1: ...ICATION testpub6 FOR TABLE testpub_rf_tbl3 WHERE (e =#> 27);
                                                              ^
 DETAIL:  User-defined operators are not allowed.
 -- fail - user-defined functions are not allowed
-CREATE FUNCTION testpub_rf_func2() RETURNS integer AS $$ BEGIN RETURN 123; END; $$ LANGUAGE plpgsql;
+CREATE FUNCTION testpub_rf_func2() RETURNS integer IMMUTABLE AS $$ BEGIN RETURN 123; END; $$ LANGUAGE plpgsql;
 ALTER PUBLICATION testpub5 ADD TABLE testpub_rf_tbl1 WHERE (a >= testpub_rf_func2());
 ERROR:  invalid publication WHERE expression
 LINE 1: ...ON testpub5 ADD TABLE testpub_rf_tbl1 WHERE (a >= testpub_rf...
@@ -523,17 +523,33 @@ Tables:
 Tables from schemas:
     "testpub_rf_schema2"
 
+-- fail - virtual generated column uses user-defined function
+CREATE TABLE testpub_rf_tbl6 (id int PRIMARY KEY, x int, y int GENERATED ALWAYS AS (x * testpub_rf_func2()) VIRTUAL);
+CREATE PUBLICATION testpub7 FOR TABLE testpub_rf_tbl6 WHERE (y > 100);
+ERROR:  invalid publication WHERE expression
+DETAIL:  User-defined or built-in mutable functions are not allowed.
+-- test that SET EXPRESSION is rejected, because it could affect a row filter
+SET client_min_messages = 'ERROR';
+CREATE TABLE testpub_rf_tbl7 (id int PRIMARY KEY, x int, y int GENERATED ALWAYS AS (x * 111) VIRTUAL);
+CREATE PUBLICATION testpub8 FOR TABLE testpub_rf_tbl7 WHERE (y > 100);
+ALTER TABLE testpub_rf_tbl7 ALTER COLUMN y SET EXPRESSION AS (x * testpub_rf_func2());
+ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables that are part of a publication
+DETAIL:  Column "y" of relation "testpub_rf_tbl7" is a virtual generated column.
+RESET client_min_messages;
 DROP TABLE testpub_rf_tbl1;
 DROP TABLE testpub_rf_tbl2;
 DROP TABLE testpub_rf_tbl3;
 DROP TABLE testpub_rf_tbl4;
 DROP TABLE testpub_rf_tbl5;
+DROP TABLE testpub_rf_tbl6;
 DROP TABLE testpub_rf_schema1.testpub_rf_tbl5;
 DROP TABLE testpub_rf_schema2.testpub_rf_tbl6;
 DROP SCHEMA testpub_rf_schema1;
 DROP SCHEMA testpub_rf_schema2;
 DROP PUBLICATION testpub5;
 DROP PUBLICATION testpub6;
+DROP PUBLICATION testpub8;
+DROP TABLE testpub_rf_tbl7;
 DROP OPERATOR =#>(integer, integer);
 DROP FUNCTION testpub_rf_func1(integer, integer);
 DROP FUNCTION testpub_rf_func2();
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index fd5654df35e..87929191d06 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -4379,6 +4379,35 @@ ERROR:  new row violates row-level security policy for table "r1"
 INSERT INTO r1 VALUES (10)
     ON CONFLICT ON CONSTRAINT r1_pkey DO UPDATE SET a = 30;
 ERROR:  new row violates row-level security policy for table "r1"
+DROP TABLE r1;
+--
+-- Test policies using virtual generated columns
+--
+SET SESSION AUTHORIZATION regress_rls_alice;
+SET row_security = on;
+CREATE TABLE r1 (a int, b int GENERATED ALWAYS AS (a * 10) VIRTUAL);
+ALTER TABLE r1 ADD c int GENERATED ALWAYS AS (a * 100) VIRTUAL;
+INSERT INTO r1 VALUES (1), (2), (4);
+CREATE POLICY p0 ON r1 USING (b * 10 = c);
+CREATE POLICY p1 ON r1 AS RESTRICTIVE USING (b > 10);
+CREATE POLICY p2 ON r1 AS RESTRICTIVE USING ((SELECT c) < 400);
+ALTER TABLE r1 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r1 FORCE ROW LEVEL SECURITY;
+-- Should fail p1
+INSERT INTO r1 VALUES (0);
+ERROR:  new row violates row-level security policy "p1" for table "r1"
+-- Should fail p2
+INSERT INTO r1 VALUES (4);
+ERROR:  new row violates row-level security policy "p2" for table "r1"
+-- OK
+INSERT INTO r1 VALUES (3);
+SELECT * FROM r1;
+ a | b  |  c  
+---+----+-----
+ 2 | 20 | 200
+ 3 | 30 | 300
+(2 rows)
+
 DROP TABLE r1;
 -- Check dependency handling
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 81e4222d26a..782d7d11dcb 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -68,6 +68,9 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated_stored join_hash
 
+# TODO: find a place for this (above group is full)
+test: generated_virtual
+
 # ----------
 # Additional BRIN tests
 # ----------
diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql
index 797e93ac714..4bc4ff465b9 100644
--- a/src/test/regress/sql/collate.icu.utf8.sql
+++ b/src/test/regress/sql/collate.icu.utf8.sql
@@ -920,6 +920,21 @@ CREATE TABLE pagg_tab6_p2 PARTITION OF pagg_tab6 FOR VALUES IN ('c', 'd');
 RESET max_parallel_workers_per_gather;
 RESET enable_incremental_sort;

 

index c18e0e1f655..d0aa91b9657 100644

 
 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
 
@@ -278,13 +292,14 @@ CREATE FUNCTION gf1(a int) RETURNS int AS $$ SELECT a * 3 $$ IMMUTABLE LANGUAGE
 
 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;
+GRANT SELECT (a, c), INSERT ON gtest12s TO regress_user11;
 
 SET ROLE regress_user11;
 SELECT a, b FROM gtest11s;  -- not allowed
 SELECT a, c FROM gtest11s;  -- allowed
 SELECT gf1(10);  -- not allowed
-SELECT a, c FROM gtest12s;  -- allowed
+INSERT INTO gtest12s VALUES (3, 30), (4, 40);  -- currently not allowed because of function permissions, should arguably be allowed
+SELECT a, c FROM gtest12s;  -- allowed (does not actually invoke the function)
 RESET ROLE;
 
@@ -390,6 +405,10 @@ CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10);
 
@@ -417,6 +436,9 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -426,6 +448,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint DEFAULT 42);
@@ -549,6 +574,18 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 
similarity index 68%
index c18e0e1f655..f97060ead47 100644

 

 \d gtest1
 
 -- duplicate generated

 -- invalid reference

 

 

 
 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,

 

 
 
--- test stored update

 -- null values
 
@@ -248,168 +265,179 @@ CREATE TABLE gtest4 (
 
 -- drop column behavior
 
 \d gtest10

 -- privileges

 
-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;
+CREATE TABLE gtest12v (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) VIRTUAL);
+INSERT INTO gtest12v VALUES (1, 10), (2, 20);
+GRANT SELECT (a, c), INSERT ON gtest12v TO regress_user11;
 
 SET ROLE regress_user11;
-SELECT a, b FROM gtest11s;  -- not allowed
-SELECT a, c FROM gtest11s;  -- allowed
+SELECT a, b FROM gtest11v;  -- not allowed
+SELECT a, c FROM gtest11v;  -- allowed
 SELECT gf1(10);  -- not allowed
-SELECT a, c FROM gtest12s;  -- allowed
+INSERT INTO gtest12v VALUES (3, 30), (4, 40);  -- allowed (does not actually invoke the function)
+SELECT a, c FROM gtest12v;  -- currently not allowed because of function permissions, should arguably be allowed
 RESET ROLE;

 -- check constraints

 
 
--- not-null constraints
 
-CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED);
+-- also check with table constraint syntax
+CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL, CONSTRAINT cc NOT NULL b);  -- error
+CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
+ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b;  -- error
+DROP TABLE gtest21ax;
+
+CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL);
 ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL;
-INSERT INTO gtest21b (a) VALUES (1);  -- ok
-INSERT INTO gtest21b (a) VALUES (0);  -- violates constraint
+--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
+--INSERT INTO gtest21b (a) VALUES (0);  -- ok now
 
 -- index constraints
 
 -- indexes
 
 -- foreign keys

 
 
-DROP TABLE gtest23b;
-DROP TABLE gtest23a;
+--DROP TABLE gtest23b;
+--DROP TABLE gtest23a;

 
 -- domains

 -- partitioning cases
 
@@ -417,6 +445,9 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -427,6 +458,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
@@ -435,6 +469,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 
@@ -457,21 +494,21 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 -- we leave these tables around for purposes of testing dump/reload/upgrade

 
@@ -479,7 +516,7 @@ CREATE TABLE gtest25 (a int PRIMARY KEY);
 CREATE TABLE gtest27 (
     a int,
     b int,
-    x int GENERATED ALWAYS AS ((a + b) * 2) STORED
+    x int GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL
 );
 INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11);
 ALTER TABLE gtest27 ALTER COLUMN a TYPE text;  -- error
@@ -493,7 +530,7 @@ CREATE TABLE gtest27 (
   DROP COLUMN x,
   ALTER COLUMN a TYPE bigint,
   ALTER COLUMN b TYPE bigint,
-  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) STORED;
+  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL;
 \d gtest27
 -- Ideally you could just do this, but not today (and should x change type?):
 ALTER TABLE gtest27
@@ -505,7 +542,7 @@ CREATE TABLE gtest27 (
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -532,7 +569,7 @@ CREATE TABLE gtest29 (
 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE gtest30 ALTER COLUMN b DROP EXPRESSION;
@@ -541,7 +578,7 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 DROP TABLE gtest30 CASCADE;
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE ONLY gtest30 ALTER COLUMN b DROP EXPRESSION;  -- error
@@ -549,10 +586,22 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 \d gtest30_1
 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION;  -- error

 CREATE FUNCTION gtest_trigger_func() RETURNS trigger
@@ -634,7 +683,7 @@ CREATE TRIGGER gtest11 BEFORE UPDATE OF b ON gtest26
 DROP TRIGGER gtest11 ON gtest26;
 TRUNCATE gtest26;
 
--- check that modifications of stored generated columns in triggers do
+-- check that modifications of generated columns in triggers do
 -- not get propagated
 CREATE FUNCTION gtest_trigger_func4() RETURNS trigger
   LANGUAGE plpgsql
@@ -667,7 +716,7 @@ CREATE TABLE gtest28a (
   a int,
   b int,
   c int,
-  x int GENERATED ALWAYS AS (b * 2) STORED
+  x int GENERATED ALWAYS AS (b * 2) VIRTUAL
 );
 
 ALTER TABLE gtest28a DROP COLUMN a;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 48e68bcca2d..aee30af035e 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -214,7 +214,7 @@ CREATE FUNCTION testpub_rf_func1(integer, integer) RETURNS boolean AS $$ SELECT
 CREATE OPERATOR =#> (PROCEDURE = testpub_rf_func1, LEFTARG = integer, RIGHTARG = integer);
 CREATE PUBLICATION testpub6 FOR TABLE testpub_rf_tbl3 WHERE (e =#> 27);
 -- fail - user-defined functions are not allowed
-CREATE FUNCTION testpub_rf_func2() RETURNS integer AS $$ BEGIN RETURN 123; END; $$ LANGUAGE plpgsql;
+CREATE FUNCTION testpub_rf_func2() RETURNS integer IMMUTABLE AS $$ BEGIN RETURN 123; END; $$ LANGUAGE plpgsql;
 ALTER PUBLICATION testpub5 ADD TABLE testpub_rf_tbl1 WHERE (a >= testpub_rf_func2());
 -- fail - non-immutable functions are not allowed. random() is volatile.
 ALTER PUBLICATION testpub5 ADD TABLE testpub_rf_tbl1 WHERE (a < random());
@@ -261,18 +261,30 @@ CREATE PUBLICATION testpub6 FOR TABLES IN SCHEMA testpub_rf_schema2;
 ALTER PUBLICATION testpub6 SET TABLES IN SCHEMA testpub_rf_schema2, TABLE testpub_rf_schema2.testpub_rf_tbl6 WHERE (i < 99);
 RESET client_min_messages;
 \dRp+ testpub6
+-- fail - virtual generated column uses user-defined function
+CREATE TABLE testpub_rf_tbl6 (id int PRIMARY KEY, x int, y int GENERATED ALWAYS AS (x * testpub_rf_func2()) VIRTUAL);
+CREATE PUBLICATION testpub7 FOR TABLE testpub_rf_tbl6 WHERE (y > 100);
+-- test that SET EXPRESSION is rejected, because it could affect a row filter
+SET client_min_messages = 'ERROR';
+CREATE TABLE testpub_rf_tbl7 (id int PRIMARY KEY, x int, y int GENERATED ALWAYS AS (x * 111) VIRTUAL);
+CREATE PUBLICATION testpub8 FOR TABLE testpub_rf_tbl7 WHERE (y > 100);
+ALTER TABLE testpub_rf_tbl7 ALTER COLUMN y SET EXPRESSION AS (x * testpub_rf_func2());
+RESET client_min_messages;
 
 DROP TABLE testpub_rf_tbl1;
 DROP TABLE testpub_rf_tbl2;
 DROP TABLE testpub_rf_tbl3;
 DROP TABLE testpub_rf_tbl4;
 DROP TABLE testpub_rf_tbl5;
+DROP TABLE testpub_rf_tbl6;
 DROP TABLE testpub_rf_schema1.testpub_rf_tbl5;
 DROP TABLE testpub_rf_schema2.testpub_rf_tbl6;
 DROP SCHEMA testpub_rf_schema1;
 DROP SCHEMA testpub_rf_schema2;
 DROP PUBLICATION testpub5;
 DROP PUBLICATION testpub6;
+DROP PUBLICATION testpub8;
+DROP TABLE testpub_rf_tbl7;
 DROP OPERATOR =#>(integer, integer);
 DROP FUNCTION testpub_rf_func1(integer, integer);
 DROP FUNCTION testpub_rf_func2();
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index cf09f62eaba..f61dbbf9581 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2072,6 +2072,33 @@ CREATE POLICY p3 ON r1 FOR INSERT WITH CHECK (true);
 
 DROP TABLE r1;
 
+--
index b1b87cf85e3..a28dc3e489b 100644

 
 # data for initial sync
@@ -42,10 +42,11 @@
 
-my $result = $node_subscriber->safe_psql('postgres', "SELECT a, b FROM tab1");
-is( $result, qq(1|22
-2|44
-3|66), 'generated columns initial sync');
+my $result =
+  $node_subscriber->safe_psql('postgres', "SELECT a, b, c FROM tab1");
+is( $result, qq(1|22|33
+2|44|66
+3|66|99), 'generated columns initial sync');
 
 # data to replicate
 
@@ -56,11 +57,11 @@

 
@@ -69,7 +70,7 @@
 
@@ -88,13 +89,13 @@ BEGIN
 
 $result =
 
 # cleanup
 $node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
diff --git a/src/test/subscription/t/028_row_filter.pl b/src/test/subscription/t/028_row_filter.pl
index 2acd36d7a6f..97cb43f450b 100644
--- a/src/test/subscription/t/028_row_filter.pl
+++ b/src/test/subscription/t/028_row_filter.pl
@@ -240,6 +240,9 @@
 $node_publisher->safe_psql('postgres',
 	"CREATE TABLE tab_rowfilter_child_sync PARTITION OF tab_rowfilter_parent_sync FOR VALUES FROM (1) TO (20)"
 );
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE tab_rowfilter_virtual (id int PRIMARY KEY, x int, y int GENERATED ALWAYS AS (x * 2) VIRTUAL)"
+);
 
 # setup structure on subscriber
 $node_subscriber->safe_psql('postgres',
@@ -294,6 +297,9 @@
 	"CREATE TABLE tab_rowfilter_parent_sync (a int)");
 $node_subscriber->safe_psql('postgres',
 	"CREATE TABLE tab_rowfilter_child_sync (a int)");
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE tab_rowfilter_virtual (id int PRIMARY KEY, x int, y int GENERATED ALWAYS AS (x * 2) VIRTUAL)"
+);
 
 # setup logical replication
 $node_publisher->safe_psql('postgres',
@@ -359,6 +365,11 @@
 	"CREATE PUBLICATION tap_pub_child_sync FOR TABLE tab_rowfilter_child_sync WHERE (a < 15)"
 );
 
+# publication using virtual generated column in row filter expression
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_virtual FOR TABLE tab_rowfilter_virtual WHERE (y > 10)"
+);
+
 #
 # The following INSERTs are executed before the CREATE SUBSCRIPTION, so these
 # SQL commands are for testing the initial data copy using logical replication.
@@ -407,8 +418,12 @@
 	"INSERT INTO tab_rowfilter_child(a, b) VALUES(0,'0'),(30,'30'),(40,'40')"
 );
 
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab_rowfilter_virtual (id, x) VALUES (1, 2), (2, 4), (3, 6)"
+);
+
 $node_subscriber->safe_psql('postgres',
-	"CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr application_name=$appname' PUBLICATION tap_pub_1, tap_pub_2, tap_pub_3, tap_pub_4a, tap_pub_4b, tap_pub_5a, tap_pub_5b, tap_pub_toast, tap_pub_inherits, tap_pub_viaroot_2, tap_pub_viaroot_1, tap_pub_parent_sync, tap_pub_child_sync"
+	"CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr application_name=$appname' PUBLICATION tap_pub_1, tap_pub_2, tap_pub_3, tap_pub_4a, tap_pub_4b, tap_pub_5a, tap_pub_5b, tap_pub_toast, tap_pub_inherits, tap_pub_viaroot_2, tap_pub_viaroot_1, tap_pub_parent_sync, tap_pub_child_sync, tap_pub_virtual"
 );
 
 # wait for initial table synchronization to finish
@@ -550,6 +565,16 @@
 	"SELECT a FROM tab_rowfilter_child_sync ORDER BY 1");
 is($result, qq(), 'check initial data copy from tab_rowfilter_child_sync');
 
+# Check expected replicated rows for tab_rowfilter_virtual
+# tap_pub_virtual filter is: (y > 10), where y is generated as (x * 2)
+# - INSERT (1, 2)      NO, 2 * 2 <= 10
+# - INSERT (2, 4)      NO, 4 * 2 <= 10
+# - INSERT (3, 6)      YES, 6 * 2 > 10
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT id, x FROM tab_rowfilter_virtual ORDER BY id");
+is($result, qq(3|6),
+	'check initial data copy from table tab_rowfilter_virtual');
+
 # The following commands are executed after CREATE SUBSCRIPTION, so these SQL
 # commands are for testing normal logical replication behavior.
 #
@@ -582,6 +607,8 @@
 	"INSERT INTO tab_rowfilter_child (a, b) VALUES (13, '13'), (17, '17')");
 $node_publisher->safe_psql('postgres',
 	"INSERT INTO tab_rowfilter_viaroot_part (a) VALUES (14), (15), (16)");
+$node_publisher->safe_psql('postgres',
+	"INSERT INTO tab_rowfilter_virtual (id, x) VALUES (4, 3), (5, 7)");
 
 $node_publisher->wait_for_catchup($appname);
 
@@ -725,6 +752,15 @@
 	'check replicated rows to tab_rowfilter_inherited and tab_rowfilter_child'
 );
 
+# Check expected replicated rows for tab_rowfilter_virtual
+# tap_pub_virtual filter is: (y > 10), where y is generated as (x * 2)
+# - INSERT (4, 3)      NO, 3 * 2 <= 10
+# - INSERT (5, 7)      YES, 7 * 2 > 10
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT id, x FROM tab_rowfilter_virtual ORDER BY id");
+is( $result, qq(3|6
+5|7), 'check replicated rows to tab_rowfilter_virtual');
+
 # UPDATE the non-toasted column for table tab_rowfilter_toast
 $node_publisher->safe_psql('postgres',
 	"UPDATE tab_rowfilter_toast SET b = '1'");

base-commit: b6612aedc53a6bf069eba5e356a8421ad6426486
-- 
2.47.0

---

From: Peter Eisentraut <[email-masked]>
Date: Fri, 29 Nov 2024 11:14:16 +0100

On 12.11.24 17:10, Peter Eisentraut wrote:
> Yes, I just need a find a good place to put it into src/test/ 
> subscription/t/028_row_filter.pl.  It's very long. ;-)

I have added tests in the v10 patch.

---

From: Amit Kapila <[email-masked]>
Date: Mon, 2 Dec 2024 14:26:07 +0530

> write the faulty expression, and it's straightforward to implement.

Fair enough but the same argument applies to the column list. I mean
to say based on the same theory, users will get the ERROR when an
unsupported virtual column type will be used in column the list.

> it.

Ok. But as mentioned above, we should consider it for the column list.

  Whether that means renaming the option or just documenting this is
> something for discussion.

We can go either way. Say, if we just document it and in the future,
if we want to support it for virtual columns then we need to introduce
another boolean option like publish_generated_virtual_columns. The
other possibility is that we change publish_generated_columns to enum
or string and allow values 's' (stored), 'v' (virtual), and 'n'
(none). Now, only 's' and 'n' will be supported. In the future, if one
wishes to add support for virtual columns, we have a provision to
extend the existing option.

-- 
With Regards,
Amit Kapila.

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 3 Dec 2024 14:37:02 +0100

> but for backpatching leave the default case in place?

I have committed this, just to master for now.

---

From: jian he <[email-masked]>
Date: Tue, 3 Dec 2024 22:15:44 +0800

-- check constraints
CREATE TABLE gtest20 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a
* 2) VIRTUAL CHECK (b < 50));
INSERT INTO gtest20 (a) VALUES (10);  -- ok
INSERT INTO gtest20 (a) VALUES (30);  -- violates constraint

ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);  --
violates constraint
ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);  -- ok
-----
The above content is in src/test/regress/sql/generated_virtual.sql,
the last two query comments
seem to conflict with the error message for now.

i add some regress tests for your v10 changes in
src/backend/commands/statscmds.c.
please check attached.

the sql tests,
"sanity check of system catalog" maybe place it to the end of the sql
file will have better chance of catching some error.
for virtual, we can also check attnotnull, atthasdef value.
like:
SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE
attgenerated IN ('v') and (attnotnull or not atthasdef);

---

From: Richard Guo <[email-masked]>
Date: Wed, 8 Jan 2025 17:22:47 +0900

> - Added support for ALTER TABLE ... SET EXPRESSION.

When using ALTER TABLE to set expression for virtual generated
columns, we don't enforce a rewrite, which means we don't have the
opportunity to check whether the new values for these columns could
cause an underflow or overflow.  For instance,

create table t (a int, b int generated always as (a) virtual);
insert into t values (2147483647);

# alter table t alter column b set expression as (a * 2);
ALTER TABLE

# select * from t;
ERROR:  integer out of range

The same thing could occur with INSERT.  As we don't compute virtual
generated columns on write, we may end up inserting values that cause
underflow or overflow for these columns.

create table t1 (a int, b int generated always as (a * 2) virtual);
insert into t1 values (2147483647);

# select * from t1;
ERROR:  integer out of range

I'm not sure if this is expected or not, so I just wanted to point it
out.

Thanks
Richard

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 8 Jan 2025 14:41:50 +0100

> I'm not sure if this is expected or not, so I just wanted to point it

Yes, this is expected behavior.  This also happens with a view.  So it 
is consistent for compute-on-read objects.

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 8 Jan 2025 17:14:30 +0100

Here is a new patch version where I have gathered various pieces of 
feedback and improvement suggestions that are scattered over this 
thread.  I hope I got them all.  I will respond to the respective 
messages directly to give my response to each item.

One thing I could use some review on is the access control handling and 
security in general.  You can create virtual generated columns that have 
their own access privileges but which can read columns that the user 
does not have access to.  Kind of like a view.  This all appears to work 
correctly, but maybe someone wants to poke a hole into it.

Here is an example:

create user foo;
create user bar;
grant create on schema public to foo;
\c - foo
create table t1 (id int, ccnum text, ccredacted text generated always as 
(repeat('*', 12) || substr(ccnum, 13, 4)) virtual);
grant select (id, ccredacted) on table t1 to bar;
insert into t1 values (1, '1234567890123456');
\c - bar
select * from t1;  -- permission denied
select id, ccredacted from t1;  -- ok
From 801cf024f688667fff4eb62a1bca6ec27c96d78b Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Wed, 8 Jan 2025 16:59:42 +0100
Subject: [PATCH v11] Virtual generated columns

TODO:

 doc/src/sgml/ref/alter_table.sgml             |  15 +-
 src/backend/commands/trigger.c                |  45 +-
 src/backend/executor/nodeModifyTable.c        |  41 +-
 .../regress/expected/generated_stored.out     | 113 ++-
 ...rated_stored.out => generated_virtual.out} | 858 +++++++++---------
 src/test/regress/expected/stats_ext.out       |  23 +
 src/test/regress/sql/generated_stored.sql     |  54 +-
 src/test/regress/sql/stats_ext.sql            |  14 +
 68 files changed, 1744 insertions(+), 777 deletions(-)
 copy src/test/regress/sql/{generated_stored.sql => generated_virtual.sql} (67%)

 drop table test8;
 
index bf322198a20..32d581e8422 100644

 
 select * from grem1;
- a  | b  
-----+----
-  1 |  2
- 22 | 44
+ a  | b  | c  
+----+----+----
+  1 |  2 |  3
+ 22 | 44 | 66
 (2 rows)
 
 delete from grem1;
 -- test copy from
 copy grem1 from stdin;
 select * from gloc1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 |  
+ 2 | 4 |  
 (2 rows)
 
 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;

 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;
index 3900522ccb5..71a27282138 100644
index cc6cf9bef09..1f5906624bd 100644
@@ -1307,8 +1307,10 @@ <title><structname>pg_attribute</structname> Columns</title>
 
index dea04d64db6..502dd154645 100644
 
   <para>
 
   <para>
index c8f7ab7d956..eff040742d2 100644
@@ -264,8 +264,8 @@ <title>Description</title>
       This form replaces the expression of a generated column.  Existing data
-      in the column is rewritten and all the future changes will apply the new
-      generation expression.
+      in a stored generated column is rewritten and all the future changes
+      will apply the new generation expression.
 
+     <para>

 
    <varlistentry>
 
      <para>
 
      <para>
index 70fa929caa4..3bff9c60ebf 100644

      <para>
 
      <para>
@@ -2459,9 +2463,9 @@ <title>Multiple Identity Columns</title>
    <title>Generated Columns</title>
 
    <para>

    <para>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index cc940742197..773a20ab3f8 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -343,6 +343,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)

@@ -640,6 +641,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
index e817f8f8f84..73b307dd08b 100644
 
 		if (att->attisdropped)
 			continue;
index 024521c66c0..2e0fd67e92d 100644
@@ -506,7 +506,7 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 
@@ -581,6 +581,17 @@ CheckAttributeType(const char *attname,
 	}
@@ -2545,6 +2556,11 @@ AddRelationNewConstraints(Relation rel,
 
 			/*
@@ -2833,6 +2849,11 @@ AddRelationNotNullConstraints(Relation rel, List *constraints,
 
 		/*
index 2a7769b1fd1..31926d7d056 100644
 
index d6e23caef17..174541f6af0 100644
 
 	/*

index 35747b3df5f..b4914ee5993 100644
@@ -687,6 +688,8 @@ TransformPubWhereClauses(List *tables, const char *queryString,
 
index a817821bf6d..e24d540cd45 100644

 
 			Assert(expr != NULL);

 			k = -1;

 			/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4181c110eb7..e65cde9c9a5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3025,6 +3025,15 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 									 errhint("A child table column cannot be generated unless its parent column is.")));
 					}
 
@@ -3297,6 +3306,15 @@ MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const
 
@@ -6098,7 +6116,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
@@ -7276,7 +7294,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,

@@ -7753,6 +7771,14 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
@@ -8379,6 +8405,8 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
@@ -8393,36 +8421,70 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,

 
 
 	/*
 
 	/*
 
 	/*
@@ -8451,7 +8513,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 
@@ -8460,16 +8522,19 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,

 
 
 	/* Drop any pg_statistic entry for the column */
 	RemoveStatistics(RelationGetRelid(rel), attnum);
@@ -8558,17 +8623,30 @@ ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMOD
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
@@ -8711,6 +8789,16 @@ ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa
 
+	/*
@@ -9903,6 +9991,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -12104,7 +12205,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 
 			/* Find or create work queue entry for this table */
 			tab = ATGetQueueEntry(wqueue, rel);
@@ -13290,8 +13391,12 @@ ATPrepAlterColumnType(List **wqueue,
 					   list_make1_oid(rel->rd_rel->reltype),
 					   0);
 
@@ -13364,11 +13469,12 @@ ATPrepAlterColumnType(List **wqueue,
 
@@ -16354,6 +16460,14 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 
@@ -18589,8 +18703,11 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 
 			/*
@@ -18672,9 +18789,12 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 				}
 
 				/*
index 32f25f4d911..e19c497bde5 100644
+static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple tuple);

 /*
+			newtuple = check_modified_virtual_generated(RelationGetDescr(relinfo->ri_RelationDesc), newtuple);
 
 			/*
@@ -3060,6 +3065,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
 		}
 		else if (newtuple != oldtuple)
 		{
+			newtuple = check_modified_virtual_generated(RelationGetDescr(relinfo->ri_RelationDesc), newtuple);
+
 			ExecForceStoreHeapTuple(newtuple, newslot, false);
 
 			/*
 
 			oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
 			tgqual = stringToNode(trigger->tgqual);
+			tgqual = (Node *) expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc, PRS2_OLD_VARNO);
+			tgqual = (Node *) expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc, PRS2_NEW_VARNO);
 			/* Change references to OLD and NEW to INNER_VAR and OUTER_VAR */
 			ChangeVarNodes(tgqual, PRS2_OLD_VARNO, INNER_VAR, 0);
 			ChangeVarNodes(tgqual, PRS2_NEW_VARNO, OUTER_VAR, 0);
@@ -6600,3 +6609,37 @@ pg_trigger_depth(PG_FUNCTION_ARGS)
 {
 	PG_RETURN_INT32(MyTriggerDepth);
 }
+
+/*
+ * Check whether a trigger modified a virtual generated column and replace the
+ * value with null if so.
+ *
+ * We need to check this so that we don't end up storing a non-null value in a
+ * virtual generated column.
+ *
+ * We don't need to check for stored generated columns, since those will be
+ * overwritten later anyway.
+ */
+static HeapTuple
+check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple tuple)
+{
+	if (!(tupdesc->constr && tupdesc->constr->has_generated_virtual))
+		return tuple;
+
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+		{
+			if (!heap_attisnull(tuple, i + 1, tupdesc))
+			{
+				int			replCol = i + 1;
+				Datum		replValue = 0;
+				bool		replIsnull = true;
+
+				tuple = heap_modify_tuple_by_cols(tuple, tupdesc, 1, &replCol, &replValue, &replIsnull);
+			}
+		}
+	}
+
+	return tuple;
+}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index b2c00a0a1b1..0abfb0357dc 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -2195,6 +2195,10 @@ CheckVarSlotCompatibility(TupleTableSlot *slot, int attnum, Oid vartype)

index a06295b6ba7..c6e84852d4d 100644

index f71899463b8..83d9e25c163 100644
@@ -1354,8 +1354,8 @@ Bitmapset *
 
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 1af8c9caf6c..486e4a4882c 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -337,11 +337,14 @@ ExecCheckTIDVisible(EState *estate,
 }
 
 /*
- * Initialize to compute stored generated columns for a tuple
+ * Initialize generated columns handling for a tuple
+ *
+ * This fills the resultRelInfo's ri_GeneratedExprsI/ri_NumGeneratedNeededI or
+ * ri_GeneratedExprsU/ri_NumGeneratedNeededU fields, depending on cmdtype.
+ * This is used only for stored generated columns.
  *
- * This fills the resultRelInfo's ri_GeneratedExprsI/ri_NumGeneratedNeededI
- * or ri_GeneratedExprsU/ri_NumGeneratedNeededU fields, depending on cmdtype.
  * If cmdType == CMD_UPDATE, the ri_extraUpdatedCols field is filled too.
+ * This is used by both stored and virtual generated columns.
  *
  * Note: usually, a given query would need only one of ri_GeneratedExprsI and
  * ri_GeneratedExprsU per result rel; but MERGE can need both, and so can
@@ -349,9 +352,9 @@ ExecCheckTIDVisible(EState *estate,
  * UPDATE and INSERT actions.
  */
 void
-ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
-						EState *estate,
-						CmdType cmdtype)
+ExecInitGenerated(ResultRelInfo *resultRelInfo,
+				  EState *estate,
+				  CmdType cmdtype)
 {
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -362,7 +365,7 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
 	MemoryContext oldContext;

 	/*
@@ -388,7 +391,9 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,

@@ -413,8 +418,11 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
 			}

 			/* If UPDATE, mark column in resultRelInfo->ri_extraUpdatedCols */
 			if (cmdtype == CMD_UPDATE)
@@ -424,6 +432,13 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
 		}
 	}
 
+	if (ri_NumGeneratedNeeded == 0)
+	{
+		/* didn't need it after all */
+		pfree(ri_GeneratedExprs);
+		ri_GeneratedExprs = NULL;
+	}
+
 	/* Save in appropriate set of fields */
 	if (cmdtype == CMD_UPDATE)
 	{
@@ -442,6 +457,8 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
 		resultRelInfo->ri_NumGeneratedNeededI = ri_NumGeneratedNeeded;
 	}

@@ -472,7 +489,7 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
@@ -480,7 +497,7 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
index b4c1e2c69dd..0c078e3e5bf 100644

 

 					/*
 
+opt_virtual_or_stored:
index 92a04e35dff..5913f5de1ec 100644
 
index d2d82c9c596..609cd6d7160 100644
 
 	/*
index 2b7499b34b9..2ee48e1b4c0 100644
@@ -1004,7 +1005,7 @@ pgoutput_row_filter_init(PGOutputData *data, List *publications,
 				continue;

index 1a5dfd0aa47..0f21ccf33be 100644

 /*
 
 		++rt_index;

 
+		/*

+/*
index 3fe74b580a5..728ded52b95 100644
@@ -592,6 +592,8 @@ RelationBuildTupleDesc(Relation relation)
 
@@ -674,6 +676,7 @@ RelationBuildTupleDesc(Relation relation)
 	 */
index 8f73a5df956..1532c27f7c1 100644
@@ -16189,6 +16189,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
index bf65d44b942..3b8fb6fe4e8 100644
index d5543fd62b0..ddd7eb16055 100644
@@ -2107,6 +2107,12 @@ describeOneTableDetails(const char *schemaname,
 
index aee871b0e8f..7b54f8275e7 100644
 
 /*
index 9dea49c52b4..9adc4ba7344 100644
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 557286f0480..3786fb52a2b 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -225,6 +225,7 @@ MAKE_SYSCACHE(ATTNUM, pg_attribute_relid_attnum_index, 128);
 #define		  ATTRIBUTE_IDENTITY_BY_DEFAULT 'd'

 
diff --git a/src/include/executor/nodeModifyTable.h b/src/include/executor/nodeModifyTable.h
index d1ddc39ad37..bf3b592e28f 100644
--- a/src/include/executor/nodeModifyTable.h
+++ b/src/include/executor/nodeModifyTable.h
@@ -15,9 +15,9 @@

 
index b3f7aa299f5..bc2d5b3f16d 100644

index 38d6ad7dcbd..811b80845db 100644
index 24c22a8694b..ed4ed0793f3 100644
index d258b26375f..88fe13c5f4f 100644

 

 

 

 
index feb6a76b56c..08c8492050e 100644

 
index d4f327636fd..b517337d6d8 100644
@@ -2531,6 +2531,26 @@ DROP TABLE pagg_tab6;

 

 
 
index 0d037d48ca0..714eefd38e2 100644
@@ -1,9 +1,4 @@
--- sanity check of system catalog
- attrelid | attname | attgenerated 
-----------+---------+--------------
-(0 rows)
 GRANT USAGE ON SCHEMA generated_stored_tests TO PUBLIC;
 SET search_path = generated_stored_tests;
@@ -128,6 +123,24 @@ SELECT * FROM gtest1 ORDER BY a;
  4 | 8
 (4 rows)

+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
@@ -312,6 +346,10 @@ ERROR:  column "b" inherits from generated column but specifies default
@@ -547,7 +585,7 @@ CREATE FUNCTION gf1(a int) RETURNS int AS $$ SELECT a * 3 $$ IMMUTABLE LANGUAGE
@@ -560,7 +598,9 @@ SELECT a, c FROM gtest11s;  -- allowed
 
@@ -764,6 +804,11 @@ CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a *
@@ -794,6 +839,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -807,6 +857,11 @@ ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09
@@ -1094,9 +1149,9 @@ SELECT * FROM gtest29;
@@ -1190,6 +1245,18 @@ Inherits: gtest30
 
 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION;  -- error
 ERROR:  cannot drop generation expression from inherited column
+-- composite type dependencies
+CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text);
+CREATE TABLE gtest31_2 (x int, y gtest31_1);
+ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
+ERROR:  cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type
+DROP TABLE gtest31_1, gtest31_2;
+-- Check it for a partitioned table, too
+CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text) PARTITION BY LIST (a);
+CREATE TABLE gtest31_2 (x int, y gtest31_1);
+ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
+ERROR:  cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type
+DROP TABLE gtest31_1, gtest31_2;
 -- triggers
 CREATE TABLE gtest26 (
     a int PRIMARY KEY,
@@ -1311,20 +1378,28 @@ BEGIN
   RETURN NEW;
 END;
 $$;
-CREATE TRIGGER gtest12_01 BEFORE UPDATE ON gtest26
+CREATE TRIGGER gtest12_01 BEFORE INSERT OR UPDATE ON gtest26
   FOR EACH ROW
   EXECUTE PROCEDURE gtest_trigger_func();
-CREATE TRIGGER gtest12_02 BEFORE UPDATE ON gtest26
+CREATE TRIGGER gtest12_02 BEFORE INSERT OR UPDATE ON gtest26
   FOR EACH ROW
   EXECUTE PROCEDURE gtest_trigger_func4();
-CREATE TRIGGER gtest12_03 BEFORE UPDATE ON gtest26
+CREATE TRIGGER gtest12_03 BEFORE INSERT OR 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 = (1,)
+INFO:  gtest12_03: BEFORE: new = (10,300)
+SELECT * FROM gtest26 ORDER BY a;
+ a  | b  
+----+----
+ 10 | 20
+(1 row)
+
+UPDATE gtest26 SET a = 11 WHERE a = 10;
+INFO:  gtest12_01: BEFORE: old = (10,20)
 INFO:  gtest12_01: BEFORE: new = (11,)
-INFO:  gtest12_03: BEFORE: old = (1,2)
+INFO:  gtest12_03: BEFORE: old = (10,20)
 INFO:  gtest12_03: BEFORE: new = (10,)
 SELECT * FROM gtest26 ORDER BY a;
  a  | b  
@@ -1356,3 +1431,9 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
  c      | integer |           |          | 
  x      | integer |           |          | generated always as (b * 2) stored
 
+-- sanity check of system catalog
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v');
+ attrelid | attname | attgenerated 
+----------+---------+--------------
+(0 rows)
+
diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_virtual.out
similarity index 67%
copy from src/test/regress/expected/generated_stored.out
copy to src/test/regress/expected/generated_virtual.out
index 0d037d48ca0..c2fb55894a4 100644
--- a/src/test/regress/expected/generated_stored.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1,15 +1,10 @@
--- sanity check of system catalog
-SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
- attrelid | attname | attgenerated 
-----------+---------+--------------
-(0 rows)
-
-CREATE SCHEMA generated_stored_tests;
-GRANT USAGE ON SCHEMA generated_stored_tests TO PUBLIC;
-SET search_path = generated_stored_tests;
-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_schema = 'generated_stored_tests' ORDER BY 1, 2;
+-- keep these tests aligned with generated_stored.sql
+CREATE SCHEMA generated_virtual_tests;
+GRANT USAGE ON SCHEMA generated_virtual_tests TO PUBLIC;
+SET search_path = generated_virtual_tests;
+CREATE TABLE gtest0 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (55) VIRTUAL);
+CREATE TABLE gtest1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
+SELECT table_name, column_name, column_default, is_nullable, is_generated, generation_expression FROM information_schema.columns WHERE table_schema = 'generated_virtual_tests' ORDER BY 1, 2;
  table_name | column_name | column_default | is_nullable | is_generated | generation_expression 
 ------------+-------------+----------------+-------------+--------------+-----------------------
  gtest0     | a           |                | NO          | NEVER        | 
@@ -18,89 +13,89 @@ SELECT table_name, column_name, column_default, is_nullable, is_generated, gener
  gtest1     | b           |                | YES         | ALWAYS       | (a * 2)
 (4 rows)

 \d gtest1
 
 -- duplicate generated
@@ -128,6 +123,24 @@ SELECT * FROM gtest1 ORDER BY a;
  4 | 8
 (4 rows)
 
@@ -153,16 +166,10 @@ SELECT a, b FROM gtest1 WHERE b = 4 ORDER BY a;
  2 | 4
 (1 row)
 
@@ -203,8 +210,8 @@ CREATE TABLE gtestm (
@@ -217,6 +224,27 @@ SELECT * FROM gtestm ORDER BY id;
   2 | 20 | 200 | 40 | 400
 (2 rows)
 
+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
@@ -273,11 +301,11 @@ SELECT * FROM gtest1_1;
 (0 rows)
 
 \d gtest1_1
 
 INSERT INTO gtest1_1 VALUES (4);
@@ -296,12 +324,12 @@ SELECT * FROM gtest1;
 
@@ -312,25 +340,44 @@ ERROR:  column "b" inherits from generated column but specifies default
 
@@ -343,28 +390,28 @@ CREATE TABLE gtest1_y () INHERITS (gtest1, gtesty);  -- error
 
@@ -381,8 +428,8 @@ TABLE gtestc;
 
@@ -403,7 +450,7 @@ SELECT * FROM gtest3 ORDER BY a;
     |   
 (4 rows)
 
@@ -468,7 +515,7 @@ SELECT * FROM gtest3 ORDER BY a;
 (4 rows)
 
 -- null values
-CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED);
+CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) VIRTUAL);
 INSERT INTO gtest2 VALUES (1);
 SELECT * FROM gtest2;
  a | b 
@@ -477,7 +524,7 @@ SELECT * FROM gtest2;
 (1 row)
 
@@ -492,7 +539,7 @@ DROP TABLE gtest_varlena;
@@ -507,11 +554,11 @@ DROP TYPE double_int;
@@ -520,7 +567,7 @@ SELECT * FROM gtest_tableoid;
 (2 rows)
 
 -- drop column behavior
@@ -528,30 +575,30 @@ HINT:  Use DROP ... CASCADE to drop the dependent objects too.
 
@@ -560,231 +607,152 @@ SELECT a, c FROM gtest11s;  -- allowed
 
-ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);  -- violates constraint
-ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);  -- ok
+ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);  -- violates constraint (currently not supported)
+ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);  -- ok (currently not supported)
@@ -794,6 +762,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -808,32 +781,37 @@ ERROR:  table "gtest_child3" being attached contains an identity column "f3"
 
 \d gtest_child2
 
 \d gtest_child3
 
@@ -844,103 +822,121 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;

 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3

 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3
 
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
@@ -953,20 +949,20 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
 
@@ -974,16 +970,16 @@ SELECT * FROM gtest25 ORDER BY a;
  4 | 12
 (2 rows)
 
@@ -992,15 +988,15 @@ SELECT * FROM gtest25 ORDER BY a;
 (2 rows)
 
 \d gtest25
 
@@ -1008,7 +1004,7 @@ Indexes:
 CREATE TABLE gtest27 (
     a int,
     b int,
@@ -1016,12 +1012,12 @@ ERROR:  cannot alter type of a column used by a generated column
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1035,20 +1031,19 @@ ERROR:  cannot specify USING when altering type of generated column
 
@@ -1057,12 +1052,12 @@ ALTER TABLE gtest27
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1074,7 +1069,7 @@ SELECT * FROM gtest27;
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -1085,18 +1080,18 @@ SELECT * FROM gtest29;
 (2 rows)
 
 \d gtest29
 
@@ -1107,93 +1102,114 @@ SELECT * FROM gtest29;
 (2 rows)
 
 \d gtest29

 \d gtest29

 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
 
 \d gtest30_1

 \d gtest30_1
 
@@ -1246,7 +1262,7 @@ CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26
@@ -1256,12 +1272,12 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
@@ -1271,8 +1287,8 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
 DELETE FROM gtest26 WHERE a = -6;
-INFO:  gtest1: BEFORE: old = (-6,-12)
-INFO:  gtest3: AFTER: old = (-6,-12)
+INFO:  gtest1: BEFORE: old = (-6,)
+INFO:  gtest3: AFTER: old = (-6,)
 SELECT * FROM gtest26 ORDER BY a;
  a | b 
 ---+---
@@ -1300,7 +1316,7 @@ 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
+-- check that modifications of generated columns in triggers do
 -- not get propagated
 CREATE FUNCTION gtest_trigger_func4() RETURNS trigger
   LANGUAGE plpgsql
@@ -1311,20 +1327,28 @@ BEGIN
   RETURN NEW;
 END;
 $$;
-CREATE TRIGGER gtest12_01 BEFORE UPDATE ON gtest26
+CREATE TRIGGER gtest12_01 BEFORE INSERT OR UPDATE ON gtest26
   FOR EACH ROW
   EXECUTE PROCEDURE gtest_trigger_func();
-CREATE TRIGGER gtest12_02 BEFORE UPDATE ON gtest26
+CREATE TRIGGER gtest12_02 BEFORE INSERT OR UPDATE ON gtest26
   FOR EACH ROW
   EXECUTE PROCEDURE gtest_trigger_func4();
-CREATE TRIGGER gtest12_03 BEFORE UPDATE ON gtest26
+CREATE TRIGGER gtest12_03 BEFORE INSERT OR 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 = (1,)
+INFO:  gtest12_03: BEFORE: new = (10,)
+SELECT * FROM gtest26 ORDER BY a;
+ a  | b  
+----+----
+ 10 | 20
+(1 row)
+
+UPDATE gtest26 SET a = 11 WHERE a = 10;
+INFO:  gtest12_01: BEFORE: old = (10,)
 INFO:  gtest12_01: BEFORE: new = (11,)
-INFO:  gtest12_03: BEFORE: old = (1,2)
+INFO:  gtest12_03: BEFORE: old = (10,)
 INFO:  gtest12_03: BEFORE: new = (10,)
 SELECT * FROM gtest26 ORDER BY a;
  a  | b  
@@ -1337,22 +1361,28 @@ CREATE TABLE gtest28a (
   a int,
   b int,
   c int,
-  x int GENERATED ALWAYS AS (b * 2) STORED
+  x int GENERATED ALWAYS AS (b * 2) VIRTUAL
 );
 ALTER TABLE gtest28a DROP COLUMN a;
 CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
 \d gtest28*
-                   Table "generated_stored_tests.gtest28a"
- Column |  Type   | Collation | Nullable |              Default               
---------+---------+-----------+----------+------------------------------------
+               Table "generated_virtual_tests.gtest28a"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
  b      | integer |           |          | 
  c      | integer |           |          | 
- x      | integer |           |          | generated always as (b * 2) stored
+ x      | integer |           |          | generated always as (b * 2)
 
-                   Table "generated_stored_tests.gtest28b"
- Column |  Type   | Collation | Nullable |              Default               
---------+---------+-----------+----------+------------------------------------
+               Table "generated_virtual_tests.gtest28b"
+ Column |  Type   | Collation | Nullable |           Default           
+--------+---------+-----------+----------+-----------------------------
  b      | integer |           |          | 
  c      | integer |           |          | 
- x      | integer |           |          | generated always as (b * 2) stored
+ x      | integer |           |          | generated always as (b * 2)
+
+-- sanity check of system catalog
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v');
+ attrelid | attname | attgenerated 
+----------+---------+--------------
+(0 rows)
 
index c48f11f2935..7cc405e002d 100644
 
+-- fail - virtual generated column uses user-defined function
+CREATE TABLE testpub_rf_tbl6 (id int PRIMARY KEY, x int, y int GENERATED ALWAYS AS (x * testpub_rf_func2()) VIRTUAL);
+CREATE PUBLICATION testpub7 FOR TABLE testpub_rf_tbl6 WHERE (y > 100);
+ERROR:  invalid publication WHERE expression
+DETAIL:  User-defined or built-in mutable functions are not allowed.
+-- test that SET EXPRESSION is rejected, because it could affect a row filter
+SET client_min_messages = 'ERROR';
+CREATE TABLE testpub_rf_tbl7 (id int PRIMARY KEY, x int, y int GENERATED ALWAYS AS (x * 111) VIRTUAL);
+CREATE PUBLICATION testpub8 FOR TABLE testpub_rf_tbl7 WHERE (y > 100);
+ALTER TABLE testpub_rf_tbl7 ALTER COLUMN y SET EXPRESSION AS (x * testpub_rf_func2());
+ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables that are part of a publication
+DETAIL:  Column "y" of relation "testpub_rf_tbl7" is a virtual generated column.
+RESET client_min_messages;
 DROP TABLE testpub_rf_tbl1;
 DROP TABLE testpub_rf_tbl2;
 DROP TABLE testpub_rf_tbl3;
 DROP TABLE testpub_rf_tbl4;
 DROP TABLE testpub_rf_tbl5;
+DROP TABLE testpub_rf_tbl6;
 DROP TABLE testpub_rf_schema1.testpub_rf_tbl5;
 DROP TABLE testpub_rf_schema2.testpub_rf_tbl6;
 DROP SCHEMA testpub_rf_schema1;
 DROP SCHEMA testpub_rf_schema2;
 DROP PUBLICATION testpub5;
 DROP PUBLICATION testpub6;
+DROP PUBLICATION testpub8;
+DROP TABLE testpub_rf_tbl7;
 DROP OPERATOR =#>(integer, integer);
 DROP FUNCTION testpub_rf_func1(integer, integer);
 DROP FUNCTION testpub_rf_func2();
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index fd5654df35e..87929191d06 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -4379,6 +4379,35 @@ ERROR:  new row violates row-level security policy for table "r1"
 INSERT INTO r1 VALUES (10)
     ON CONFLICT ON CONSTRAINT r1_pkey DO UPDATE SET a = 30;
 ERROR:  new row violates row-level security policy for table "r1"
+DROP TABLE r1;
+--
+-- Test policies using virtual generated columns
+--
+SET SESSION AUTHORIZATION regress_rls_alice;
+SET row_security = on;
+CREATE TABLE r1 (a int, b int GENERATED ALWAYS AS (a * 10) VIRTUAL);
+ALTER TABLE r1 ADD c int GENERATED ALWAYS AS (a * 100) VIRTUAL;
+INSERT INTO r1 VALUES (1), (2), (4);
+CREATE POLICY p0 ON r1 USING (b * 10 = c);
+CREATE POLICY p1 ON r1 AS RESTRICTIVE USING (b > 10);
+CREATE POLICY p2 ON r1 AS RESTRICTIVE USING ((SELECT c) < 400);
+ALTER TABLE r1 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r1 FORCE ROW LEVEL SECURITY;
+-- Should fail p1
+INSERT INTO r1 VALUES (0);
+ERROR:  new row violates row-level security policy "p1" for table "r1"
+-- Should fail p2
+INSERT INTO r1 VALUES (4);
+ERROR:  new row violates row-level security policy "p2" for table "r1"
+-- OK
+INSERT INTO r1 VALUES (3);
+SELECT * FROM r1;
+ a | b  |  c  
+---+----+-----
+ 2 | 20 | 200
+ 3 | 30 | 300
+(2 rows)
+
 DROP TABLE r1;
 -- Check dependency handling
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index a4c7be487ef..9a820404d3f 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -66,6 +66,29 @@ ERROR:  syntax error at or near ","
 LINE 1: CREATE STATISTICS tst ON (x, y) FROM ext_stats_test;
                                    ^
 DROP TABLE ext_stats_test;
+-- statistics on virtual generated column not allowed
+CREATE TABLE ext_stats_test1 (x int, y int, z int GENERATED ALWAYS AS (x+y) VIRTUAL, w xid);
+CREATE STATISTICS tst on z from ext_stats_test1;
+ERROR:  statistics creation on virtual generated columns is not supported
+CREATE STATISTICS tst on (z) from ext_stats_test1;
+ERROR:  statistics creation on virtual generated columns is not supported
+CREATE STATISTICS tst on (z+1) from ext_stats_test1;
+ERROR:  statistics creation on virtual generated columns is not supported
+CREATE STATISTICS tst (ndistinct) ON z from ext_stats_test1;
+ERROR:  statistics creation on virtual generated columns is not supported
+-- statistics on system column not allowed
+CREATE STATISTICS tst on tableoid from ext_stats_test1;
+ERROR:  statistics creation on system columns is not supported
+CREATE STATISTICS tst on (tableoid) from ext_stats_test1;
+ERROR:  statistics creation on system columns is not supported
+CREATE STATISTICS tst on (tableoid::int+1) from ext_stats_test1;
+ERROR:  statistics creation on system columns is not supported
+CREATE STATISTICS tst (ndistinct) ON xmin from ext_stats_test1;
+ERROR:  statistics creation on system columns is not supported
+-- statistics without a less-than operator not supported
+CREATE STATISTICS tst (ndistinct) ON w from ext_stats_test1;
+ERROR:  column "w" cannot be used in statistics because its type xid has no default btree operator class
+DROP TABLE ext_stats_test1;
 -- Ensure stats are dropped sanely, and test IF NOT EXISTS while at it
 CREATE TABLE ab1 (a INTEGER, b INTEGER, c INTEGER);
 CREATE STATISTICS IF NOT EXISTS ab1_a_b_stats ON a, b FROM ab1;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1edd9e45ebb..7d096c7a294 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -68,6 +68,9 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity generated_stored join_hash
 
+# TODO: find a place for this (above group is full)
+test: generated_virtual
+
 # ----------
 # Additional BRIN tests
 # ----------
diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql
index 5ee2da4e0e0..a7c7a14031a 100644
--- a/src/test/regress/sql/collate.icu.utf8.sql
+++ b/src/test/regress/sql/collate.icu.utf8.sql
@@ -950,6 +950,21 @@ CREATE TABLE pagg_tab6_p2 PARTITION OF pagg_tab6 FOR VALUES IN ('c', 'd');
 RESET max_parallel_workers_per_gather;
 RESET enable_incremental_sort;

 

 -- Test a large sample of different datatypes
diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql
index c18e0e1f655..2aeac459445 100644
--- a/src/test/regress/sql/generated_stored.sql
+++ b/src/test/regress/sql/generated_stored.sql
@@ -1,5 +1,4 @@
--- sanity check of system catalog
-SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
+-- keep these tests aligned with generated_virtual.sql

 CREATE SCHEMA generated_stored_tests;
@@ -60,6 +59,8 @@ CREATE TABLE gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT AS (a *
 INSERT INTO gtest1 VALUES (3, DEFAULT), (4, DEFAULT);  -- ok

 UPDATE gtest1 SET b = DEFAULT WHERE a = 1;
@@ -103,6 +104,14 @@ CREATE TABLE gtestm (
 SELECT * FROM gtestm ORDER BY id;
 DROP TABLE gtestm;
 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
@@ -142,6 +151,7 @@ CREATE TABLE gtest_normal_child (a int, b int GENERATED ALWAYS AS (a * 2) STORED
 
@@ -278,13 +288,14 @@ CREATE FUNCTION gf1(a int) RETURNS int AS $$ SELECT a * 3 $$ IMMUTABLE LANGUAGE

 SET ROLE regress_user11;
 
@@ -390,6 +401,10 @@ CREATE DOMAIN gtestdomain1 AS int CHECK (VALUE < 10);
 
@@ -417,6 +432,9 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -426,6 +444,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint DEFAULT 42);
@@ -549,6 +570,18 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 
@@ -646,20 +679,21 @@ CREATE FUNCTION gtest_trigger_func4() RETURNS trigger
 END;
 $$;
 
-CREATE TRIGGER gtest12_01 BEFORE UPDATE ON gtest26
+CREATE TRIGGER gtest12_01 BEFORE INSERT OR UPDATE ON gtest26
   FOR EACH ROW
   EXECUTE PROCEDURE gtest_trigger_func();
 
-CREATE TRIGGER gtest12_02 BEFORE UPDATE ON gtest26
+CREATE TRIGGER gtest12_02 BEFORE INSERT OR UPDATE ON gtest26
   FOR EACH ROW
   EXECUTE PROCEDURE gtest_trigger_func4();
 
-CREATE TRIGGER gtest12_03 BEFORE UPDATE ON gtest26
+CREATE TRIGGER gtest12_03 BEFORE INSERT OR 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;
+UPDATE gtest26 SET a = 11 WHERE a = 10;
 SELECT * FROM gtest26 ORDER BY a;
 
 -- LIKE INCLUDING GENERATED and dropped column handling
@@ -675,3 +709,7 @@ CREATE TABLE gtest28a (
 CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
 
 \d gtest28*
+
+
+-- sanity check of system catalog
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v');
diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_virtual.sql
similarity index 67%
copy from src/test/regress/sql/generated_stored.sql
copy to src/test/regress/sql/generated_virtual.sql
index c18e0e1f655..b5ae20caa7c 100644
--- a/src/test/regress/sql/generated_stored.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -1,55 +1,54 @@
--- sanity check of system catalog
-SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
+-- keep these tests aligned with generated_stored.sql

 

 \d gtest1
 
 -- duplicate generated

 -- invalid reference

 

 
 INSERT INTO gtest1 VALUES (1);
 INSERT INTO gtest1 VALUES (2, DEFAULT);  -- ok
@@ -60,6 +59,8 @@ CREATE TABLE gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT AS (a *
 INSERT INTO gtest1 VALUES (3, DEFAULT), (4, DEFAULT);  -- ok

 UPDATE gtest1 SET b = DEFAULT WHERE a = 1;
@@ -70,7 +71,7 @@ CREATE TABLE gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT AS (a *
 SELECT a, b, b * 2 AS b2 FROM gtest1 ORDER BY a;
 SELECT a, b FROM gtest1 WHERE b = 4 ORDER BY a;
 
@@ -93,8 +94,8 @@ CREATE TABLE gtestm (
@@ -103,6 +104,14 @@ CREATE TABLE gtestm (
 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
@@ -134,22 +143,26 @@ CREATE TABLE gtest1_1 () INHERITS (gtest1);

 
 
 -- test multiple inheritance mismatches
@@ -161,28 +174,28 @@ CREATE TABLE gtesty (x int, b int);
 CREATE TABLE gtest1_y () INHERITS (gtest1, gtesty);  -- error
 DROP TABLE gtesty;

 
--- test stored update
 
@@ -222,12 +235,12 @@ CREATE TABLE gtest3a (a text, b text GENERATED ALWAYS AS (a || '+' || a) STORED)
 
 -- null values
 
@@ -237,7 +250,7 @@ CREATE TABLE gtest_varlena (a varchar, b varchar GENERATED ALWAYS AS (a) STORED)
@@ -248,168 +261,179 @@ CREATE TABLE gtest4 (
 
 -- drop column behavior
 
 \d gtest10

 -- privileges

 
 
 SET ROLE regress_user11;

 -- check constraints
 
-ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);  -- violates constraint
-ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);  -- ok
+ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100);  -- violates constraint (currently not supported)
+ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3);  -- ok (currently not supported)

 
--- not-null constraints

 -- index constraints
 
 -- indexes
 
 -- foreign keys

 
 
-DROP TABLE gtest23b;
-DROP TABLE gtest23a;
+--DROP TABLE gtest23b;
+--DROP TABLE gtest23a;

 
 -- domains

 -- partitioning cases
 
@@ -417,6 +441,9 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -427,6 +454,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
@@ -435,6 +465,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 
@@ -457,21 +490,21 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 -- we leave these tables around for purposes of testing dump/reload/upgrade

 
@@ -479,7 +512,7 @@ CREATE TABLE gtest25 (a int PRIMARY KEY);
 CREATE TABLE gtest27 (
     a int,
     b int,
-    x int GENERATED ALWAYS AS ((a + b) * 2) STORED
+    x int GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL
 );
 INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11);
 ALTER TABLE gtest27 ALTER COLUMN a TYPE text;  -- error
@@ -493,7 +526,7 @@ CREATE TABLE gtest27 (
   DROP COLUMN x,
   ALTER COLUMN a TYPE bigint,
   ALTER COLUMN b TYPE bigint,
-  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) STORED;
+  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL;
 \d gtest27
 -- Ideally you could just do this, but not today (and should x change type?):
 ALTER TABLE gtest27
@@ -505,7 +538,7 @@ CREATE TABLE gtest27 (
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -532,7 +565,7 @@ CREATE TABLE gtest29 (
 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE gtest30 ALTER COLUMN b DROP EXPRESSION;
@@ -541,7 +574,7 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 DROP TABLE gtest30 CASCADE;
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE ONLY gtest30 ALTER COLUMN b DROP EXPRESSION;  -- error
@@ -549,10 +582,22 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 \d gtest30_1
 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION;  -- error

 CREATE FUNCTION gtest_trigger_func() RETURNS trigger
@@ -634,7 +679,7 @@ CREATE TRIGGER gtest11 BEFORE UPDATE OF b ON gtest26
 DROP TRIGGER gtest11 ON gtest26;
 TRUNCATE gtest26;
 
--- check that modifications of stored generated columns in triggers do
+-- check that modifications of generated columns in triggers do
 -- not get propagated
 CREATE FUNCTION gtest_trigger_func4() RETURNS trigger
   LANGUAGE plpgsql
@@ -646,20 +691,21 @@ CREATE FUNCTION gtest_trigger_func4() RETURNS trigger
 END;
 $$;
 
-CREATE TRIGGER gtest12_01 BEFORE UPDATE ON gtest26
+CREATE TRIGGER gtest12_01 BEFORE INSERT OR UPDATE ON gtest26
   FOR EACH ROW
   EXECUTE PROCEDURE gtest_trigger_func();
 
-CREATE TRIGGER gtest12_02 BEFORE UPDATE ON gtest26
+CREATE TRIGGER gtest12_02 BEFORE INSERT OR UPDATE ON gtest26
   FOR EACH ROW
   EXECUTE PROCEDURE gtest_trigger_func4();
 
-CREATE TRIGGER gtest12_03 BEFORE UPDATE ON gtest26
+CREATE TRIGGER gtest12_03 BEFORE INSERT OR 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;
+UPDATE gtest26 SET a = 11 WHERE a = 10;
 SELECT * FROM gtest26 ORDER BY a;
 
 -- LIKE INCLUDING GENERATED and dropped column handling
@@ -667,7 +713,7 @@ CREATE TABLE gtest28a (
   a int,
   b int,
   c int,
-  x int GENERATED ALWAYS AS (b * 2) STORED
+  x int GENERATED ALWAYS AS (b * 2) VIRTUAL
 );
 
 ALTER TABLE gtest28a DROP COLUMN a;
@@ -675,3 +721,7 @@ CREATE TABLE gtest28a (
 CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
 
 \d gtest28*
+
+
+-- sanity check of system catalog
index c4c21a95d0e..3b03fb647f1 100644

 DROP TABLE r1;
 
+--
+-- Test policies using virtual generated columns
+--
+SET SESSION AUTHORIZATION regress_rls_alice;
+SET row_security = on;
+CREATE TABLE r1 (a int, b int GENERATED ALWAYS AS (a * 10) VIRTUAL);
+ALTER TABLE r1 ADD c int GENERATED ALWAYS AS (a * 100) VIRTUAL;
+INSERT INTO r1 VALUES (1), (2), (4);
+
+CREATE POLICY p0 ON r1 USING (b * 10 = c);
+CREATE POLICY p1 ON r1 AS RESTRICTIVE USING (b > 10);
+CREATE POLICY p2 ON r1 AS RESTRICTIVE USING ((SELECT c) < 400);
+ALTER TABLE r1 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE r1 FORCE ROW LEVEL SECURITY;
+
+-- Should fail p1
+INSERT INTO r1 VALUES (0);
+
+-- Should fail p2
+INSERT INTO r1 VALUES (4);
+
+-- OK
+INSERT INTO r1 VALUES (3);
+SELECT * FROM r1;
+
+DROP TABLE r1;
+
 -- Check dependency handling
 RESET SESSION AUTHORIZATION;
 CREATE TABLE dep1 (c1 int);
diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql
index 5c786b16c6f..75b04e5a136 100644
--- a/src/test/regress/sql/stats_ext.sql
+++ b/src/test/regress/sql/stats_ext.sql
@@ -45,6 +45,20 @@ CREATE STATISTICS tst ON (y) FROM ext_stats_test; -- single column reference
 CREATE STATISTICS tst ON y + z FROM ext_stats_test; -- missing parentheses
 CREATE STATISTICS tst ON (x, y) FROM ext_stats_test; -- tuple expression
 DROP TABLE ext_stats_test;
+-- statistics on virtual generated column not allowed
+CREATE TABLE ext_stats_test1 (x int, y int, z int GENERATED ALWAYS AS (x+y) VIRTUAL, w xid);
+CREATE STATISTICS tst on z from ext_stats_test1;
+CREATE STATISTICS tst on (z) from ext_stats_test1;
+CREATE STATISTICS tst on (z+1) from ext_stats_test1;
+CREATE STATISTICS tst (ndistinct) ON z from ext_stats_test1;
+-- statistics on system column not allowed
+CREATE STATISTICS tst on tableoid from ext_stats_test1;
+CREATE STATISTICS tst on (tableoid) from ext_stats_test1;
+CREATE STATISTICS tst on (tableoid::int+1) from ext_stats_test1;
+CREATE STATISTICS tst (ndistinct) ON xmin from ext_stats_test1;
+-- statistics without a less-than operator not supported
+CREATE STATISTICS tst (ndistinct) ON w from ext_stats_test1;
+DROP TABLE ext_stats_test1;
 
 -- Ensure stats are dropped sanely, and test IF NOT EXISTS while at it
 CREATE TABLE ab1 (a INTEGER, b INTEGER, c INTEGER);
diff --git a/src/test/subscription/t/011_generated.pl b/src/test/subscription/t/011_generated.pl
index 45587371400..6e73db7b417 100644
--- a/src/test/subscription/t/011_generated.pl
+++ b/src/test/subscription/t/011_generated.pl
@@ -21,11 +21,11 @@
 my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';

 
 # data for initial sync
@@ -42,10 +42,11 @@

 # data to replicate
 
@@ -56,11 +57,11 @@

 
@@ -69,7 +70,7 @@

 $result =
 
 # cleanup
index e3a6d69da7e..e2c83670053 100644

 

 

@@ -725,6 +752,15 @@

base-commit: 7b27f5fd36cb3270e8ac25aefd73b552663d1392
-- 
2.47.1

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 8 Jan 2025 17:25:07 +0100

> seem to conflict with the error message for now.

Fixed the comment in the test in patch v11.

> src/backend/commands/statscmds.c.

Added to patch v11.

> attgenerated IN ('v') and (attnotnull or not atthasdef);

I moved the existing check to the bottom, as you suggest.

I don't understand what the purpose of testing attnotnull is.  That is 
independent of attgenerated, I think.

---

From: Tom Lane <[email-masked]>
Date: Wed, 08 Jan 2025 11:38:58 -0500

> independent of attgenerated, I think.

Does it make any sense to set NOT NULL on a generated column (virtual
or otherwise, but especially virtual)?  What is the system supposed
to do if the expression evaluates to null?  That concern generalizes
to any constraint really.  Even if we checked it at row storage time,
there's no real guarantee that the expression is immutable enough
to pass the constraint later.

			regards, tom lane

---

From: Marcos Pegoraro <[email-masked]>
Date: Wed, 8 Jan 2025 16:19:11 -0300

> messages directly to give my response to each item.

This new version you are not accepting subqueries, like previous ones. But
we can create an immutable SQL function which will do the same. Wouldn't it
be better to explain that on DOCs ?

create table Orders(Order_ID integer not null primary key, Customer_ID
integer references Customer);
create function lkCustomer(integer) returns text language sql immutable as
$function$select Name from Customer where Customer_ID = $1;$function$;
alter table Orders add lkCustomer text generated always as
(lkCustomer(Customer_ID)) stored;

regards
Marcos

---

From: Vik Fearing <[email-masked]>
Date: Wed, 8 Jan 2025 20:23:35 +0100

On 08/01/2025 20:19, Marcos Pegoraro wrote:
> Em qua., 8 de jan. de 2025 às 13:14, Peter Eisentraut 
> <[email-masked]> escreveu:
>
> This new version you are not accepting subqueries, like previous ones. 
> But we can create an immutable SQL function which will do the same. 
> Wouldn't it be better to explain that on DOCs ?
>
> create function lkCustomer(integer) returns text language sql 
> immutable as $function$select Name from Customer where Customer_ID = 
> $1;$function$;

This is lying to the planner, and you get to enjoy whatever breaks 
because of it.  A function that accesses external data is not immutable; 
it is stable at best.

-- 

Vik Fearing

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 8 Jan 2025 20:28:33 +0100

> to pass the constraint later.

The generation expression is required to be immutable.  So a table 
definition like

    a int,
    b int generated always as (a * 2) virtual,
    check (b > 0)

is not very different from

    a int,
    check (a * 2 > 0)

in terms of the constraint execution.

The current patch does not support not-null constraints, but that's 
mostly because it's not implemented yet.  Maybe that's what Jian was 
thinking about.

---

From: Marcos Pegoraro <[email-masked]>
Date: Wed, 8 Jan 2025 16:29:51 -0300

> because of it.  A function that accesses external data is not immutable;

I understand that, but it's not documented, so users can think that way is
fine. So, it would be good to explain why this way could break this or that.

regards
Marcos

---

From: jian he <[email-masked]>
Date: Thu, 9 Jan 2025 16:38:02 +0800

> select id, ccredacted from t1;  -- ok

I think this is expected.
however once the user can access the pg_catalog,
then he can use pg_get_expr
figure out the generation expression.

so here "bar" can figure out the column value of ccnum, i think.

---

From: jian he <[email-masked]>
Date: Thu, 9 Jan 2025 20:41:09 +0800

> mostly because it's not implemented yet.  Maybe that's what Jian was
yes.
we have 4 four appearance of
errmsg("not-null constraints are not supported on virtual generated columns")
which means there are many ways to not-null constraint to virtual
generated columns.
But in the current patch, the virtual generated column cannot be not-null,
that's why i add attnotnull check.

we can not ALTER COLUMN DROP EXPRESSION for virtual for now.
so the following comments in generated_virtual.sql conflict with the output.
```
-- check that dependencies between columns have also been removed
ALTER TABLE gtest29 DROP COLUMN a;  -- should not drop b
\d gtest29
```
we can also comment out line 557 in generated_virtual.sql.

attach patch is removing unnecessary parentheses from
```ereport(ERROR, (errcode```
only for this patch related.
per commit https://git.postgresql.org/cgit/postgresql.git/commit/?id=e3a87b4991cc2d00b7a3082abb54c5f12baedfd1

also https://www.postgresql.org/docs/current/error-message-reporting.html says
 ""The extra parentheses were required before PostgreSQL version 12,
but are now optional.""

---

From: Dean Rasheed <[email-masked]>
Date: Thu, 9 Jan 2025 13:08:00 +0000

> correctly, but maybe someone wants to poke a hole into it.

That looks correct to me. Permissions are checked on the columns
mentioned in the query, not whatever columns the virtual generated
column's expression refers to. If it were a view, there'd be
additional checks that the view owner had the required privileges on
the referenced columns, but for virtual columns in a table, there is
no separate view owner, so no additional checks are necessary.

> select id, ccredacted from t1;  -- ok

Makes sense.

Regards,
Dean

---

From: Dean Rasheed <[email-masked]>
Date: Mon, 13 Jan 2025 18:15:15 +0000

On Wed, 8 Jan 2025 at 16:14, Peter Eisentraut <[email-masked]> wrote:
>
> Here is a new patch version

+       RangeTblEntry *rte;
+

This dummy RTE is a bit too minimal.

I think it should explicitly set rte->rtekind to RTE_RELATION, even
though that's technically not necessary since RTE_RELATION is zero.

In addition, it needs to set rte->eref, because expandRTE() (called
from ReplaceVarsFromTargetList()) needs that when expanding whole-row
variables. Here's a simple reproducer which crashes:

CREATE TABLE foo (a int, b int GENERATED ALWAYS AS (a*2) VIRTUAL);
ALTER TABLE foo ADD CONSTRAINT foo_check CHECK (foo IS NOT NULL);

Regards,
Dean

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 14 Jan 2025 10:42:37 +0100

> so here "bar" can figure out the column value of ccnum, i think.

Having access to the expression definition doesn't help you reverse the 
computation, if the computation itself is not reversible.

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 14 Jan 2025 10:58:20 +0100

> we can also comment out line 557 in generated_virtual.sql.

Ok will do.

>   ""The extra parentheses were required before PostgreSQL version 12,

Right.  I tried to keep it mostly consistent with existing surrounding 
code, but I will do another pass to improve that.

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 14 Jan 2025 14:37:49 +0100

> ALTER TABLE foo ADD CONSTRAINT foo_check CHECK (foo IS NOT NULL);

Thanks, fixed.  Here is a new patch with that fixed and also a few 
tweaks suggested by Jian.

I've also added a patch that addresses logical replication.  It 
basically adds back some of the prohibitions against including generated 
columns in publications that have been lifted, but this time only for 
virtual generated columns, and amends the documentation.  It doesn't 
rename the publication option "publish_generated_columns", but maybe 
that should be done.
From 3ae0b17a06ac5632564ccca7db254e9d822a916f Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Tue, 14 Jan 2025 11:25:20 +0100
Subject: [PATCH v12 1/2] Virtual generated columns

TODO:
- catversion

 src/backend/rewrite/rewriteHandler.c          | 118 ++-
 .../regress/expected/generated_stored.out     | 120 ++-
 ...rated_stored.out => generated_virtual.out} | 864 +++++++++---------
 src/test/regress/sql/generated_stored.sql     |  60 +-
 ...rated_stored.sql => generated_virtual.sql} | 348 ++++---
 68 files changed, 1771 insertions(+), 784 deletions(-)
 copy src/test/regress/sql/{generated_stored.sql => generated_virtual.sql} (66%)

 drop table test8;

 
 
 select * from grem1;
- a  | b  
-----+----
-  1 |  2
- 22 | 44
+ a  | b  | c  
+----+----+----
+  1 |  2 |  3
+ 22 | 44 | 66
 (2 rows)
 
 delete from grem1;
 -- test copy from
 copy grem1 from stdin;
 select * from gloc1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 |  
+ 2 | 4 |  
 (2 rows)
 
 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;

 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;
index 238ed679190..cc5104d1d5f 100644

   <para>
 
   <para>
index 938450fba18..29c513f76da 100644
 
+     <para>
index 0dcd9ca6f87..e0b0e075c2c 100644
 [ ENFORCED | NOT ENFORCED ]
 
 <phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
@@ -283,7 +283,7 @@ <title>Parameters</title>
    </varlistentry>
 
    <varlistentry>
-    <term><literal>GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED</literal><indexterm><primary>generated column</primary></indexterm></term>
+    <term><literal>GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) [ STORED | VIRTUAL ]</literal><indexterm><primary>generated column</primary></indexterm></term>
     <listitem>
      <para>
       This clause creates the column as a <firstterm>generated
@@ -292,10 +292,13 @@ <title>Parameters</title>
      </para>
 
      <para>
 
      <para>
index 2237321cb4f..060793404f1 100644

      <para>
 
      <para>
@@ -2489,9 +2493,9 @@ <title>Multiple Identity Columns</title>
    <title>Generated Columns</title>
 
    <para>

    <para>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index fe197447912..ed2195f14b2 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -342,6 +342,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)

 
 		if (att->attisdropped)
 			continue;
index 57ef466acce..956f196fc95 100644
@@ -507,7 +507,7 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 
@@ -582,6 +582,17 @@ CheckAttributeType(const char *attname,
 	}
 	else if (att_typtype == TYPTYPE_DOMAIN)
 	{
+		/*
+		 * Prevent virtual generated columns from having a domain type.  We
+		 * would have to enforce domain constraints when columns underlying
+		 * the generated column change.  This could possibly be implemented,
+		 * but it's not.
+		 */
+		if (flags & CHKATYPE_IS_VIRTUAL)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("virtual generated column \"%s\" cannot have a domain type", attname));
+
 		/*
 		 * If it's a domain, recurse to check its base type.
 		 */
@@ -2553,6 +2564,11 @@ AddRelationNewConstraints(Relation rel,
 						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						errmsg("cannot add not-null constraint on system column \"%s\"",
 							   strVal(linitial(cdef->keys))));
+			/* TODO: see transformColumnDefinition() */
+			if (get_attgenerated(RelationGetRelid(rel), colnum) == ATTRIBUTE_GENERATED_VIRTUAL)
+				ereport(ERROR,
+						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						errmsg("not-null constraints are not supported on virtual generated columns"));
 
 			/*
 			 * If the column already has a not-null constraint, we don't want
@@ -2868,6 +2884,11 @@ AddRelationNotNullConstraints(Relation rel, List *constraints,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("cannot add not-null constraint on system column \"%s\"",
 						   strVal(linitial(constr->keys))));
+		/* TODO: see transformColumnDefinition() */
+		if (get_attgenerated(RelationGetRelid(rel), attnum) == ATTRIBUTE_GENERATED_VIRTUAL)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("not-null constraints are not supported on virtual generated columns"));
 
 		/*

 	/*

 

 
 			Assert(expr != NULL);

 			k = -1;

 			/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4fc54bd6eba..22541941b43 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3027,6 +3027,15 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 									 errhint("A child table column cannot be generated unless its parent column is.")));
 					}
 
@@ -3312,6 +3321,15 @@ MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const
 
@@ -6118,7 +6136,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
@@ -7296,7 +7314,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,

@@ -7773,6 +7791,14 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
@@ -8399,6 +8425,8 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
@@ -8413,36 +8441,70 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,

 
 
 	/*
 
 	/*
 
 	/*
@@ -8471,7 +8533,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 
@@ -8480,16 +8542,19 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,

 
 
 	/* Drop any pg_statistic entry for the column */
 	RemoveStatistics(RelationGetRelid(rel), attnum);
@@ -8578,17 +8643,30 @@ ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMOD
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
@@ -8731,6 +8809,16 @@ ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa
 
+	/*
@@ -9923,6 +10011,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -12130,7 +12231,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 
 			/* Find or create work queue entry for this table */
 			tab = ATGetQueueEntry(wqueue, rel);
@@ -13316,8 +13417,12 @@ ATPrepAlterColumnType(List **wqueue,
 					   list_make1_oid(rel->rd_rel->reltype),
 					   0);
 
@@ -13390,11 +13495,12 @@ ATPrepAlterColumnType(List **wqueue,
 
@@ -16383,6 +16489,14 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 
@@ -18630,8 +18744,11 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 
 			/*
@@ -18713,9 +18830,12 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 				}
 
 				/*
index acf3e4a3f1f..b0e35f65a0e 100644

 /*
@@ -2504,6 +2507,8 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
 
 			/*
@@ -3061,6 +3066,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
 		}
 		else if (newtuple != oldtuple)
 		{
+			newtuple = check_modified_virtual_generated(RelationGetDescr(relinfo->ri_RelationDesc), newtuple);
+
 			ExecForceStoreHeapTuple(newtuple, newslot, false);
 
 			/*
@@ -3491,6 +3498,8 @@ TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
 
@@ -6601,3 +6610,37 @@ pg_trigger_depth(PG_FUNCTION_ARGS)

index 2d28ec65fc4..82d51bb9aff 100644

 
 
 /*

 	/*

 

 
 
index 6079de70e09..fe4a5d718cb 100644
@@ -630,7 +630,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -778,7 +778,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

 
@@ -3989,7 +3989,7 @@ ColConstraintElem:
 					n->location = @1;
 					$$ = (Node *) n;
 				}
-			| GENERATED generated_when AS '(' a_expr ')' STORED
+			| GENERATED generated_when AS '(' a_expr ')' opt_virtual_or_stored
 				{
 					Constraint *n = makeNode(Constraint);
 
@@ -3997,6 +3997,7 @@ ColConstraintElem:
 					n->generated_when = $2;
 					n->raw_expr = $5;
 					n->cooked_expr = NULL;
+					n->generated_kind = $7;
 					n->location = @1;
 
 					/*
@@ -4044,6 +4045,12 @@ generated_when:
 			| BY DEFAULT	{ $$ = ATTRIBUTE_IDENTITY_BY_DEFAULT; }
 		;
 
+opt_virtual_or_stored:
@@ -17947,6 +17954,7 @@ unreserved_keyword:
@@ -18602,6 +18610,7 @@ bare_label_keyword:
 
index ca028d2a66d..eb7716cd84c 100644
@@ -988,6 +988,20 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 
 	/*

index 1a5dfd0aa47..023d77046bb 100644

 /*
 
 		++rt_index;

 
+		/*
 
@@ -4419,6 +4434,99 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
 }

+/*
+		/* eref needs to be set, but the actual name doesn't matter */
+		rte->eref = makeAlias(RelationGetRelationName(rel), NIL);
+		rte->rtekind = RTE_RELATION;
index 43219a9629c..398114373e9 100644

diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index ff27df9e9a6..396eeb7a0bb 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -44,6 +44,7 @@ typedef struct TupleConstr
 	uint16		num_check;
 	bool		has_not_null;
 	bool		has_generated_stored;
+	bool		has_generated_virtual;
 } TupleConstr;
 
 /*
index cad830dc39c..19c594458bd 100644

 

 

 
index b191eaaecab..de17b21299a 100644
@@ -2768,6 +2768,7 @@ typedef struct Constraint
index cf2917ad07e..40cf090ce61 100644
@@ -491,6 +491,7 @@ PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)

 

 

 

 

 
index e0613891351..2cebe382432 100644

 

 
 
index 0d037d48ca0..77242838b26 100644

+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
 
 SELECT gf1(10);  -- not allowed
 ERROR:  permission denied for function gf1
-SELECT a, c FROM gtest12s;  -- allowed
+INSERT INTO gtest12s VALUES (3, 30), (4, 40);  -- currently not allowed because of function permissions, should arguably be allowed
+ERROR:  permission denied for function gf1
+SELECT a, c FROM gtest12s;  -- allowed (does not actually invoke the function)
  a | c  
 ---+----
  1 | 30
@@ -595,6 +635,13 @@ 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" of relation "gtest20b" is violated by some row
+-- check with whole-row reference
+CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
+ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL);
+INSERT INTO gtest20c VALUES (1);  -- ok
+INSERT INTO gtest20c VALUES (NULL);  -- fails
+ERROR:  new row for relation "gtest20c" violates check constraint "whole_row_check"
+DETAIL:  Failing row contains (null, null).
 -- 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
@@ -764,6 +811,11 @@ CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a *
 INSERT INTO gtest24 (a) VALUES (4);  -- ok
 INSERT INTO gtest24 (a) VALUES (6);  -- error
 ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
+CREATE TYPE gtestdomain1range AS range (subtype = gtestdomain1);
+CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) STORED);
+INSERT INTO gtest24r (a) VALUES (4);  -- ok
+INSERT INTO gtest24r (a) VALUES (6);  -- error
+ERROR:  value for domain gtestdomain1 violates check constraint "gtestdomain1_check"
 -- 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);
@@ -794,6 +846,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
     f3 WITH OPTIONS GENERATED ALWAYS AS IDENTITY  -- error
 ) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 ERROR:  identity columns are not supported on partitions
+CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
+    f3 GENERATED ALWAYS AS (f2 * 2) VIRTUAL  -- error
+) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
+ERROR:  column "f3" inherits from generated column of different kind
+DETAIL:  Parent column is STORED, child column is VIRTUAL.
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
 ERROR:  column "f3" in child table must be a generated column
@@ -807,6 +864,11 @@ ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09
 ERROR:  table "gtest_child3" being attached contains an identity column "f3"
 DETAIL:  The new partition may not contain an identity column.
 DROP TABLE gtest_child3;
+CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) VIRTUAL);
+ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
+ERROR:  column "f3" inherits from generated column of different kind
+DETAIL:  Parent column is STORED, child column is VIRTUAL.
+DROP TABLE gtest_child3;
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) STORED);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 \d gtest_child
@@ -1094,9 +1156,9 @@ SELECT * FROM gtest29;
 ALTER TABLE gtest29 ALTER COLUMN a SET EXPRESSION AS (a * 3);  -- error
 ERROR:  column "a" of relation "gtest29" is not a generated column
 ALTER TABLE gtest29 ALTER COLUMN a DROP EXPRESSION;  -- error
-ERROR:  column "a" of relation "gtest29" is not a stored generated column
+ERROR:  column "a" of relation "gtest29" is not a 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
+NOTICE:  column "a" of relation "gtest29" is not a generated column, skipping
 -- Change the expression
 ALTER TABLE gtest29 ALTER COLUMN b SET EXPRESSION AS (a * 3);
 SELECT * FROM gtest29;
@@ -1190,6 +1252,18 @@ Inherits: gtest30
 
@@ -1311,20 +1385,28 @@ BEGIN
@@ -1356,3 +1438,9 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
 
index 0d037d48ca0..5a74c0a0947 100644

 \d gtest1
 
 -- duplicate generated

 
+DROP TABLE gtestm;
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,
 
 \d gtest1_1

 

 
 
 -- null values

 -- drop column behavior
 
@@ -560,231 +607,159 @@ SELECT a, c FROM gtest11s;  -- allowed
 
+-- check with whole-row reference
+CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
+ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL);
+INSERT INTO gtest20c VALUES (1);  -- ok
+INSERT INTO gtest20c VALUES (NULL);  -- fails
+ERROR:  new row for relation "gtest20c" violates check constraint "whole_row_check"
+DETAIL:  Failing row contains (null, virtual).
@@ -794,6 +769,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -808,32 +788,37 @@ ERROR:  table "gtest_child3" being attached contains an identity column "f3"
 
 \d gtest_child2
 
 \d gtest_child3
 
@@ -844,103 +829,121 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;

 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3

 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3
 
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
@@ -953,20 +956,20 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
 
@@ -974,16 +977,16 @@ SELECT * FROM gtest25 ORDER BY a;
  4 | 12
 (2 rows)
 
@@ -992,15 +995,15 @@ SELECT * FROM gtest25 ORDER BY a;
 (2 rows)
 
 \d gtest25
 
@@ -1008,7 +1011,7 @@ Indexes:
 CREATE TABLE gtest27 (
     a int,
     b int,
@@ -1016,12 +1019,12 @@ ERROR:  cannot alter type of a column used by a generated column
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1035,20 +1038,19 @@ ERROR:  cannot specify USING when altering type of generated column
 
@@ -1057,12 +1059,12 @@ ALTER TABLE gtest27
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1074,7 +1076,7 @@ SELECT * FROM gtest27;
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -1085,18 +1087,18 @@ SELECT * FROM gtest29;
 (2 rows)
 
 \d gtest29
 
@@ -1107,93 +1109,105 @@ SELECT * FROM gtest29;
 (2 rows)
 
 \d gtest29
 
-ALTER TABLE gtest29 ALTER COLUMN b DROP EXPRESSION;
+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.
 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;
  a | b  
 ---+----
  3 |  9
  4 | 12
- 5 |   
- 6 | 66
-(4 rows)
+ 5 | 15
+(3 rows)
 
 \d gtest29
 
-ALTER TABLE gtest29 DROP COLUMN a;  -- should not drop b
-\d gtest29
+--ALTER TABLE gtest29 DROP COLUMN a;  -- should not drop b
+--\d gtest29
 
 \d gtest30_1

 \d gtest30_1
 
@@ -1246,7 +1260,7 @@ CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26
@@ -1256,12 +1270,12 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
@@ -1271,8 +1285,8 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
@@ -1300,7 +1314,7 @@ UPDATE gtest26 SET a = 1 WHERE a = 0;
@@ -1311,20 +1325,28 @@ BEGIN
@@ -1337,22 +1359,28 @@ CREATE TABLE gtest28a (

 

index a41f8b83d77..63a60303659 100644

index c18e0e1f655..77c13306a58 100644

 
 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,

 
 SET ROLE regress_user11;
 
 DROP FUNCTION gf1(int);  -- fail
@@ -311,6 +322,12 @@ CREATE TABLE gtest20b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STOR
 ALTER TABLE gtest20b ADD CONSTRAINT chk CHECK (b < 50) NOT VALID;
 ALTER TABLE gtest20b VALIDATE CONSTRAINT chk;  -- fails on existing row
 
+-- check with whole-row reference
+CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
+ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL);
+INSERT INTO gtest20c VALUES (1);  -- ok
+INSERT INTO gtest20c VALUES (NULL);  -- fails
+
 -- 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
@@ -390,6 +407,10 @@ 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
+CREATE TYPE gtestdomain1range AS range (subtype = gtestdomain1);
+CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) STORED);
+INSERT INTO gtest24r (a) VALUES (4);  -- ok
+INSERT INTO gtest24r (a) VALUES (6);  -- error
 
@@ -426,6 +450,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint DEFAULT 42);
@@ -549,6 +576,18 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 
@@ -646,20 +685,21 @@ CREATE FUNCTION gtest_trigger_func4() RETURNS trigger
 END;
 $$;

 

 -- LIKE INCLUDING GENERATED and dropped column handling
@@ -675,3 +715,7 @@ CREATE TABLE gtest28a (
 CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
 
 \d gtest28*
+
+
similarity index 66%
index c18e0e1f655..f855898caa8 100644

 

 \d gtest1
 
 -- duplicate generated

 -- invalid reference

 

 

 
 
+CREATE TABLE gtestm (
+  a int PRIMARY KEY,

 

 
 
--- test stored update

 -- null values
 
@@ -248,168 +261,185 @@ CREATE TABLE gtest4 (
 
 -- drop column behavior
 
 \d gtest10

 -- privileges

 
 
 SET ROLE regress_user11;

 -- check constraints

 
 
--- 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
+-- check with whole-row reference
+CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
+ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL);
+INSERT INTO gtest20c VALUES (1);  -- ok
+INSERT INTO gtest20c VALUES (NULL);  -- fails
+
+-- not-null constraints (currently not supported)
+CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) VIRTUAL NOT NULL);
+--INSERT INTO gtest21a (a) VALUES (1);  -- ok
+--INSERT INTO gtest21a (a) VALUES (0);  -- violates constraint

 -- index constraints
 
 -- indexes
 
 -- foreign keys

 
 
-DROP TABLE gtest23b;
-DROP TABLE gtest23a;
+--DROP TABLE gtest23b;
+--DROP TABLE gtest23a;

 
 -- domains

 -- partitioning cases
 
@@ -417,6 +447,9 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -427,6 +460,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
@@ -435,6 +471,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 
@@ -457,21 +496,21 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 -- we leave these tables around for purposes of testing dump/reload/upgrade

 
@@ -479,7 +518,7 @@ CREATE TABLE gtest25 (a int PRIMARY KEY);
 CREATE TABLE gtest27 (
     a int,
     b int,
-    x int GENERATED ALWAYS AS ((a + b) * 2) STORED
+    x int GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL
 );
 INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11);
 ALTER TABLE gtest27 ALTER COLUMN a TYPE text;  -- error
@@ -493,7 +532,7 @@ CREATE TABLE gtest27 (
   DROP COLUMN x,
   ALTER COLUMN a TYPE bigint,
   ALTER COLUMN b TYPE bigint,
-  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) STORED;
+  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL;
 \d gtest27
 -- Ideally you could just do this, but not today (and should x change type?):
 ALTER TABLE gtest27
@@ -505,7 +544,7 @@ CREATE TABLE gtest27 (
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -519,20 +558,20 @@ CREATE TABLE gtest29 (
 SELECT * FROM gtest29;
 \d gtest29
 
-ALTER TABLE gtest29 ALTER COLUMN b DROP EXPRESSION;
+ALTER TABLE gtest29 ALTER COLUMN b DROP EXPRESSION;  -- not supported
 INSERT INTO gtest29 (a) VALUES (5);
 INSERT INTO gtest29 (a, b) VALUES (6, 66);
 SELECT * FROM gtest29;
 \d gtest29
 
 -- check that dependencies between columns have also been removed
-ALTER TABLE gtest29 DROP COLUMN a;  -- should not drop b
-\d gtest29
+--ALTER TABLE gtest29 DROP COLUMN a;  -- should not drop b
+--\d gtest29
 
 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE gtest30 ALTER COLUMN b DROP EXPRESSION;
@@ -541,7 +580,7 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 DROP TABLE gtest30 CASCADE;
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE ONLY gtest30 ALTER COLUMN b DROP EXPRESSION;  -- error
@@ -549,10 +588,22 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 \d gtest30_1
 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION;  -- error

 CREATE FUNCTION gtest_trigger_func() RETURNS trigger
@@ -634,7 +685,7 @@ CREATE TRIGGER gtest11 BEFORE UPDATE OF b ON gtest26
 DROP TRIGGER gtest11 ON gtest26;
 TRUNCATE gtest26;
 
--- check that modifications of stored generated columns in triggers do
+-- check that modifications of generated columns in triggers do
 -- not get propagated
 CREATE FUNCTION gtest_trigger_func4() RETURNS trigger
   LANGUAGE plpgsql
@@ -646,20 +697,21 @@ CREATE FUNCTION gtest_trigger_func4() RETURNS trigger
 END;
 $$;

 

 -- LIKE INCLUDING GENERATED and dropped column handling
@@ -667,7 +719,7 @@ CREATE TABLE gtest28a (
   a int,
   b int,
   c int,
-  x int GENERATED ALWAYS AS (b * 2) STORED
+  x int GENERATED ALWAYS AS (b * 2) VIRTUAL
 );
 
 ALTER TABLE gtest28a DROP COLUMN a;
@@ -675,3 +727,7 @@ CREATE TABLE gtest28a (
 CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
 
 \d gtest28*
+
+

 DROP TABLE r1;
 
+--

 
 
 # data for initial sync
@@ -42,10 +42,11 @@

 # data to replicate
 
@@ -56,11 +57,11 @@

 
@@ -69,7 +70,7 @@

 $result =
 
 # cleanup

 

 

@@ -725,6 +752,15 @@

base-commit: ce9a74707d4cf7768cff06298d09c7f7e823341d
-- 
2.47.1

From 146018b9b78de2b858a97748217c4554eee28b3c Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Tue, 14 Jan 2025 14:20:52 +0100
Subject: [PATCH v12 2/2] Logical replication vs. virtual generated columns

Commits 745217a051a and 7054186c4eb (with subsequent amendments in
5b0c46ea093, 8fcd80258bc, 87ce27de696) added support for using
(stored) generated columns in logical replication.  But this doesn't
work for virtual generated columns, because their value is not
available in logical decoding.  This patch re-enables the original
prohibitions, but this time for virtual generated columns only.

The publication option "publish_generated_columns" and some related
internal variables are not renamed, but maybe they should be.

Discussion: https://www.postgresql.org/message-id/flat/[email-masked]
---
 doc/src/sgml/ddl.sgml                       |  4 +++-
 doc/src/sgml/logical-replication.sgml       |  2 +-
 doc/src/sgml/ref/create_publication.sgml    |  6 +++---
 src/backend/catalog/pg_publication.c        | 21 +++++++++++++++++----
 src/backend/replication/logical/proto.c     |  5 +++++
 src/backend/replication/logical/relation.c  |  4 ++--
 src/backend/replication/logical/tablesync.c |  4 ++--
 src/backend/replication/pgoutput/pgoutput.c |  2 +-
 src/test/regress/expected/publication.out   |  9 +++++++--
 src/test/regress/sql/publication.sql        |  8 ++++++--
 src/test/subscription/t/031_column_list.pl  | 14 ++++++++------
 11 files changed, 55 insertions(+), 24 deletions(-)

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 502dd154645..16dc0212669 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -539,11 +539,13 @@ <title>Generated Columns</title>
     </listitem>
     <listitem>
      <para>
-      Generated columns are allowed to be replicated during logical replication
+      Stored generated columns are allowed to be replicated during logical replication
       according to the <command>CREATE PUBLICATION</command> parameter
       <link linkend="sql-createpublication-params-with-publish-generated-columns">
       <literal>publish_generated_columns</literal></link> or by including them
       in the column list of the <command>CREATE PUBLICATION</command> command.
+      Virtual generated columns are skipped for logical replication and cannot be
+      specified in a <command>CREATE PUBLICATION</command> column list.
      </para>
     </listitem>
    </itemizedlist>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 8290cd1a083..83678da40a8 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -1438,7 +1438,7 @@ <title>Column Lists</title>
    copied.  However, if the subscriber is from a release prior to 15, then
    all the columns in the table are copied during initial data synchronization,
    ignoring any column lists. If the subscriber is from a release prior to 18,
-   then initial table synchronization won't copy generated columns even if they
+   then initial table synchronization won't copy stored generated columns even if they
    are defined in the publisher.
   </para>
 
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 5e25536554a..f61cea59725 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -89,7 +89,7 @@ <title>Parameters</title>
 
      <para>
       When a column list is specified, only the named columns are replicated.
-      The column list can contain generated columns as well. If no column list
+      The column list can contain stored generated columns as well. If no column list
       is specified, all table columns (except generated columns) are replicated
       through this publication, including any columns added later. It has no
       effect on <literal>TRUNCATE</literal> commands. See
@@ -193,7 +193,7 @@ <title>Parameters</title>
         <term><literal>publish_generated_columns</literal> (<type>boolean</type>)</term>
         <listitem>
          <para>
-          Specifies whether the generated columns present in the tables
+          Specifies whether the stored generated columns present in the tables
           associated with the publication should be replicated.
           The default is <literal>false</literal>.
          </para>
@@ -201,7 +201,7 @@ <title>Parameters</title>
          <note>
           <para>
            If the subscriber is from a release prior to 18, then initial table
-           synchronization won't copy generated columns even if parameter
+           synchronization won't copy stored generated columns even if parameter
            <literal>publish_generated_columns</literal> is true in the
            publisher.
           </para>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index b89098f5e99..c56937d7204 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -546,7 +546,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
  * pub_collist_validate
  *		Process and validate the 'columns' list and ensure the columns are all
  *		valid to use for a publication.  Checks for and raises an ERROR for
- * 		any unknown columns, system columns, or duplicate columns.
+ *		any unknown columns, system columns, duplicate columns, or virtual
+ *		generated columns.
  *
  * Looks up each column's attnum and returns a 0-based Bitmapset of the
  * corresponding attnums.
@@ -556,6 +557,7 @@ pub_collist_validate(Relation targetrel, List *columns)
 {
 	Bitmapset  *set = NULL;
 	ListCell   *lc;
+	TupleDesc   tupdesc = RelationGetDescr(targetrel);
 
 	foreach(lc, columns)
 	{
@@ -574,6 +576,12 @@ pub_collist_validate(Relation targetrel, List *columns)
 					errmsg("cannot use system column \"%s\" in publication column list",
 						   colname));
 
+		if (TupleDescAttr(tupdesc, attnum - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					errmsg("cannot use virtual generated column \"%s\" in publication column list",
+						   colname));
+
 		if (bms_is_member(attnum, set))
 			ereport(ERROR,
 					errcode(ERRCODE_DUPLICATE_OBJECT),
@@ -622,7 +630,8 @@ pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols, MemoryContext mcxt)
 /*
  * Returns a bitmap representing the columns of the specified table.
  *
- * Generated columns are included if include_gencols is true.
+ * Stored generated columns are included if include_gencols is true.  Virtual
+ * generated columns are never included.
  */
 Bitmapset *
 pub_form_cols_map(Relation relation, bool include_gencols)
@@ -634,7 +643,9 @@ pub_form_cols_map(Relation relation, bool include_gencols)
 	{
 		Form_pg_attribute att = TupleDescAttr(desc, i);
 
-		if (att->attisdropped || (att->attgenerated && !include_gencols))
+		if (att->attisdropped ||
+			att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ||
+			(att->attgenerated && !include_gencols))
 			continue;
 
 		result = bms_add_member(result, att->attnum);
@@ -1276,7 +1287,9 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			{
 				Form_pg_attribute att = TupleDescAttr(desc, i);
 
-				if (att->attisdropped || (att->attgenerated && !pub->pubgencols))
+				if (att->attisdropped ||
+					att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ||
+					(att->attgenerated && !pub->pubgencols))
 					continue;
 
 				attnums[nattnums++] = att->attnum;
diff --git a/src/backend/replication/logical/proto.c b/src/backend/replication/logical/proto.c
index bef350714db..6b412019731 100644
--- a/src/backend/replication/logical/proto.c
+++ b/src/backend/replication/logical/proto.c
@@ -1260,6 +1260,8 @@ logicalrep_message_type(LogicalRepMsgType action)
  *
  * Note that generated columns can be published only when present in a
  * publication column list, or when include_gencols is true.
+ *
+ * Virtual generated columns are never published.
  */
 bool
 logicalrep_should_publish_column(Form_pg_attribute att, Bitmapset *columns,
@@ -1268,6 +1270,9 @@ logicalrep_should_publish_column(Form_pg_attribute att, Bitmapset *columns,
 	if (att->attisdropped)
 		return false;
 
+	if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+		return false;
+
 	/* If a column list is provided, publish only the cols in that list. */
 	if (columns)
 		return bms_is_member(att->attnum, columns);
diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c
index 67aab02ff76..1fea66a2604 100644
--- a/src/backend/replication/logical/relation.c
+++ b/src/backend/replication/logical/relation.c
@@ -455,8 +455,8 @@ logicalrep_rel_open(LogicalRepRelId remoteid, LOCKMODE lockmode)
 			entry->attrmap->attnums[i] = attnum;
 			if (attnum >= 0)
 			{
-				/* Remember which subscriber columns are generated. */
-				if (attr->attgenerated)
+				/* Remember which subscriber columns are stored generated. */
+				if (attr->attgenerated == ATTRIBUTE_GENERATED_STORED)
 					generatedattrs = bms_add_member(generatedattrs, attnum);
 
 				missingatts = bms_del_member(missingatts, attnum);
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 6af5c9fe16c..68ab03a8e6b 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -947,9 +947,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 					 "       a.atttypid,"
 					 "       a.attnum = ANY(i.indkey)");
 
-	/* Generated columns can be replicated since version 18. */
+	/* Stored generated columns can be replicated since version 18. */
 	if (server_version >= 180000)
-		appendStringInfo(&cmd, ", a.attgenerated != ''");
+		appendStringInfo(&cmd, ", a.attgenerated = '%c'", ATTRIBUTE_GENERATED_STORED);
 
 	appendStringInfo(&cmd,
 					 "  FROM pg_catalog.pg_attribute a"
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 2ee48e1b4c0..4a9ce8243aa 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1035,7 +1035,7 @@ check_and_init_gencol(PGOutputData *data, List *publications,
 	{
 		Form_pg_attribute att = TupleDescAttr(desc, i);
 
-		if (att->attgenerated)
+		if (att->attgenerated == ATTRIBUTE_GENERATED_STORED)
 		{
 			gencolpresent = true;
 			break;
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 7cc405e002d..c11ffa5a889 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -727,7 +727,9 @@ CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
 CREATE PUBLICATION testpub_fortable_insert WITH (publish = 'insert');
 RESET client_min_messages;
 CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text,
-	d int generated always as (a + length(b)) stored);
+    d int generated always as (a + length(b)) stored,
+    e int generated always as (a + length(b)) virtual
+);
 -- error: column "x" does not exist
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);
 ERROR:  column "x" of relation "testpub_tbl5" does not exist
@@ -764,9 +766,12 @@ UPDATE testpub_tbl5 SET a = 1;
 ERROR:  cannot update table "testpub_tbl5"
 DETAIL:  Column list used by the publication does not cover the replica identity.
 ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
--- ok: generated column "d" can be in the list too
+-- ok: stored generated column "d" can be in the list too
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, d);
 ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
+-- error: virtual generated column "e" can't be in list
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, e);
+ERROR:  cannot use virtual generated column "e" in publication column list
 -- error: change the replica identity to "b", and column list to (a, c)
 -- then update fails, because (a, c) does not cover replica identity
 ALTER TABLE testpub_tbl5 REPLICA IDENTITY USING INDEX testpub_tbl5_b_key;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 3b03fb647f1..d05f21a8472 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -447,7 +447,9 @@ CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
 CREATE PUBLICATION testpub_fortable_insert WITH (publish = 'insert');
 RESET client_min_messages;
 CREATE TABLE testpub_tbl5 (a int PRIMARY KEY, b text, c text,
-	d int generated always as (a + length(b)) stored);
+    d int generated always as (a + length(b)) stored,
+    e int generated always as (a + length(b)) virtual
+);
 -- error: column "x" does not exist
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, x);
 -- error: replica identity "a" not included in the column list
@@ -474,9 +476,11 @@ CREATE UNIQUE INDEX testpub_tbl5_b_key ON testpub_tbl5 (b, c);
 UPDATE testpub_tbl5 SET a = 1;
 ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
 
--- ok: generated column "d" can be in the list too
+-- ok: stored generated column "d" can be in the list too
 ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, d);
 ALTER PUBLICATION testpub_fortable DROP TABLE testpub_tbl5;
+-- error: virtual generated column "e" can't be in list
+ALTER PUBLICATION testpub_fortable ADD TABLE testpub_tbl5 (a, e);
 
 -- error: change the replica identity to "b", and column list to (a, c)
 -- then update fails, because (a, c) does not cover replica identity
diff --git a/src/test/subscription/t/031_column_list.pl b/src/test/subscription/t/031_column_list.pl
index 7a535e76b08..b16dea9aa17 100644
--- a/src/test/subscription/t/031_column_list.pl
+++ b/src/test/subscription/t/031_column_list.pl
@@ -1202,14 +1202,16 @@
 is( $result, qq(t
 t), 'check the number of columns in the old tuple');
 
-# TEST: Dropped columns are not considered for the column list, and generated
-# columns are not replicated if they are not explicitly included in the column
-# list. So, the publication having a column list except for those columns and a
-# publication without any column list (aka all columns as part of the columns
-# list) are considered to have the same column list.
+# TEST: Dropped columns and virtual generated columns are not
+# considered for the column list, and stored generated columns are not
+# replicated if they are not explicitly included in the column
+# list. So, the publication having a column list except for those
+# columns and a publication without any column list (aka all columns
+# as part of the columns list) are considered to have the same column
+# list.
 $node_publisher->safe_psql(
 	'postgres', qq(
-	CREATE TABLE test_mix_4 (a int PRIMARY KEY, b int, c int, d int GENERATED ALWAYS AS (a + 1) STORED);
+	CREATE TABLE test_mix_4 (a int PRIMARY KEY, b int, c int, d int GENERATED ALWAYS AS (a + 1) STORED, e int GENERATED ALWAYS AS (a + 2) VIRTUAL);
 	ALTER TABLE test_mix_4 DROP COLUMN c;
 
 	CREATE PUBLICATION pub_mix_7 FOR TABLE test_mix_4 (a, b);
-- 
2.47.1

---

From: vignesh C <[email-masked]>
Date: Wed, 15 Jan 2025 12:41:39 +0530

> rename the publication option "publish_generated_columns", but maybe

There are two potential approaches we could take to address the
"publish_generated_columns" option: a) We could support multiple
values for publish_generated_columns, such as 'none', 'stored', and
'virtual', as Amit suggested in [1]. b) Alternatively, we could rename
publish_generated_columns to publish_stored_generated_columns.
Both options seem reasonable to me. Do you have a preference for which
approach would be better?

[1] - https://www.postgresql.org/message-id/[email-masked]

Regards,
Vignesh

---

From: Dean Rasheed <[email-masked]>
Date: Wed, 15 Jan 2025 14:12:52 +0000

On Tue, 14 Jan 2025 at 13:37, Peter Eisentraut <[email-masked]> wrote:
>
> Here is a new patch with that fixed and also a few

I'm hoping to push my RETURNING OLD/NEW patch [1] soon, so I thought
that I would check how it works together with this patch. The good
news is that AFAICS everything just works, and it's possible to return
old/new virtual generated columns in DML queries as expected.

It did require a minor update, because my patch adds a new
"result_relation" argument to ReplaceVarsFromTargetList() -- needed in
DML queries because, when propagating a Var's old/new
varreturningtype, replacement Vars need to be handled differently
depending on whether or not they refer to the result relation. So that
affects expand_generated_columns_internal(), when called from
fireRIRrules(). OTOH, from expand_generated_columns_in_expr() it's OK
to just pass 0 as the result relation index, because there won't be
any old/new Vars in an expression that's not part of a DML query.

Attached is the delta patch I used to handle this, along with a couple
of simple test cases. It doesn't really matter which feature makes it
in first, but the one that comes second will need to do something like
this.

Regards,
Dean

[1] https://commitfest.postgresql.org/51/4723/

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 15 Jan 2025 20:37:58 +0100

> in first, but the one that comes second will need to do something like

Ok, I'll wait if you want to go ahead with yours soon.

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 15 Jan 2025 20:46:26 +0100

> approach would be better?

I have a very weak preference for a).

---

From: vignesh C <[email-masked]>
Date: Thu, 16 Jan 2025 14:21:20 +0530

> I have a very weak preference for a).

Thanks for the suggestion, I have posted a patch for this at [1].
[1] - https://www.postgresql.org/message-id/CALDaNm3OcXdY0EzDEKAfaK9gq2B67Mfsgxu93%2B_249ohyts%3D0g%40mail.gmail.com

Regards,
Vignesh

---

From: Shlok Kyal <[email-masked]>
Date: Thu, 23 Jan 2025 17:22:18 +0530

> rename the publication option "publish_generated_columns", but maybe

Hi Peter,

I tried to apply the patch on HEAD but it is not applying.
Rebase is required because of recent commits.

Thanks and Regards,
Shlok Kyal

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 27 Jan 2025 10:59:04 +0100

> Ok, I'll wait if you want to go ahead with yours soon.

Here is an updated patch that integrates the above changes and also 
makes some adjustments now that the logical replication configuration 
questions are resolved.  I think this is complete now.

But I'm seeing mysterious CI failures that have me stumped.  For example:

https://cirrus-ci.com/task/5924251028422656

I have seen this particular pgbench test failure sporadically but 
several times, and I have not seen it anywhere without this patch, and 
never locally.  The macOS task on the cfbot CI is very flaky right now, 
so it's hard to get a good baseline.  Also, it seems to me that this 
failing test could not possibly be further away from the code that the 
patch changes, so I'm thinking timing problems, but it only happens on 
the macOS task.  Really weird.
From 4574d76e2ba167d6bd36901e9362c2b3b997bc1d Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Mon, 27 Jan 2025 07:42:55 +0100
Subject: [PATCH v13] Virtual generated columns

- virtual columns are not supported in logical replication

Reviewed-by: Jian He <[email-masked]>
Reviewed-by: Dean Rasheed <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/[email-masked]
---
 contrib/pageinspect/expected/page.out         |  37 +
 contrib/pageinspect/sql/page.sql              |  19 +
 .../postgres_fdw/expected/postgres_fdw.out    |  81 +-
 contrib/postgres_fdw/sql/postgres_fdw.sql     |   9 +-
 doc/src/sgml/catalogs.sgml                    |   6 +-
 doc/src/sgml/ddl.sgml                         |  46 +-
 doc/src/sgml/ref/alter_table.sgml             |  15 +-
 doc/src/sgml/ref/create_foreign_table.sgml    |  11 +-
 doc/src/sgml/ref/create_table.sgml            |  22 +-
 doc/src/sgml/trigger.sgml                     |   4 +
 src/backend/access/common/tupdesc.c           |   3 +
 src/backend/access/heap/heapam_handler.c      |   2 +
 src/backend/catalog/heap.c                    |  23 +-
 src/backend/catalog/pg_publication.c          |  10 +-
 src/backend/commands/analyze.c                |   4 +
 src/backend/commands/indexcmds.c              |  33 +-
 src/backend/commands/publicationcmds.c        |   3 +
 src/backend/commands/statscmds.c              |  20 +-
 src/backend/commands/tablecmds.c              | 196 ++++-
 src/backend/commands/trigger.c                |  45 +-
 src/backend/executor/execExprInterp.c         |   4 +
 src/backend/executor/execMain.c               |   5 +-
 src/backend/executor/execUtils.c              |   4 +-
 src/backend/executor/nodeModifyTable.c        |  41 +-
 src/backend/parser/gram.y                     |  15 +-
 src/backend/parser/parse_relation.c           |   6 +-
 src/backend/parser/parse_utilcmd.c            |  16 +-
 src/backend/replication/pgoutput/pgoutput.c   |   3 +-
 src/backend/rewrite/rewriteHandler.c          | 133 ++-
 src/backend/utils/cache/relcache.c            |   3 +
 src/bin/pg_dump/pg_dump.c                     |   3 +
 src/bin/pg_dump/t/002_pg_dump.pl              |   6 +-
 src/bin/psql/describe.c                       |   6 +
 src/include/access/tupdesc.h                  |   1 +
 src/include/catalog/catversion.h              |   2 +-
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/pg_attribute.h            |   1 +
 src/include/executor/nodeModifyTable.h        |   6 +-
 src/include/nodes/execnodes.h                 |   3 +
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/rewrite/rewriteHandler.h          |   2 +
 src/pl/plperl/expected/plperl_trigger.out     |   7 +-
 src/pl/plperl/plperl.c                        |   3 +
 src/pl/plperl/sql/plperl_trigger.sql          |   3 +-
 src/pl/plpython/expected/plpython_trigger.out |   7 +-
 src/pl/plpython/plpy_typeio.c                 |   3 +
 src/pl/plpython/sql/plpython_trigger.sql      |   3 +-
 src/pl/tcl/expected/pltcl_trigger.out         |  19 +-
 src/pl/tcl/pltcl.c                            |   3 +
 src/pl/tcl/sql/pltcl_trigger.sql              |   3 +-
 .../regress/expected/collate.icu.utf8.out     |  20 +
 .../regress/expected/create_table_like.out    |  23 +-
 src/test/regress/expected/fast_default.out    |  12 +
 .../regress/expected/generated_stored.out     |  21 +-
 ...rated_stored.out => generated_virtual.out} | 754 ++++++++----------
 src/test/regress/expected/publication.out     |  27 +-
 src/test/regress/expected/rowsecurity.out     |  29 +
 src/test/regress/expected/stats_ext.out       |  23 +
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/collate.icu.utf8.sql     |  15 +
 src/test/regress/sql/create_table_like.sql    |   2 +-
 src/test/regress/sql/fast_default.sql         |  11 +
 src/test/regress/sql/generated_stored.sql     |  12 +-
 ...rated_stored.sql => generated_virtual.sql} | 300 +++----
 src/test/regress/sql/publication.sql          |  22 +-
 src/test/regress/sql/rowsecurity.sql          |  27 +
 src/test/regress/sql/stats_ext.sql            |  14 +
 src/test/subscription/t/011_generated.pl      |  39 +-
 src/test/subscription/t/028_row_filter.pl     |  38 +-
 src/test/subscription/t/031_column_list.pl    |   2 +-
 71 files changed, 1530 insertions(+), 766 deletions(-)
 copy src/test/regress/expected/{generated_stored.out => generated_virtual.out} (72%)
 copy src/test/regress/sql/{generated_stored.sql => generated_virtual.sql} (74%)

 drop table test8;
 
index 85252cbdbcf..6341f64601b 100644
@@ -7395,65 +7395,68 @@ select * from rem1;

 
 select * from grem1;
- a  | b  
-----+----
-  1 |  2
- 22 | 44
+ a  | b  | c  
+----+----+----
+  1 |  2 |  3
+ 22 | 44 | 66
 (2 rows)
 
 delete from grem1;
 -- test copy from
 copy grem1 from stdin;
 select * from gloc1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 |  
+ 2 | 4 |  
 (2 rows)
 
 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;
@@ -7461,28 +7464,28 @@ delete from grem1;

 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;
index b58ab6ee586..1598d9e0862 100644
@@ -1859,12 +1859,15 @@ CREATE VIEW rw_view AS SELECT * FROM parent_tbl WHERE a < 5 WITH CHECK OPTION;
index 8ad0ed10b3a..0b5d883fbbb 100644
 
index dea04d64db6..482785370f5 100644
 
   <para>
 
   <para>
@@ -519,6 +544,7 @@ <title>Generated Columns</title>
+      This is currently only supported for stored generated columns.
index f9576da435e..8e56b8e59b0 100644
 
+     <para>

    <varlistentry>
 
      <para>
 
      <para>

      <para>
 
      <para>
 
    <para>
 
index c3c0faf7a1b..e9214dcf1b1 100644
@@ -293,6 +293,10 @@ <title>Overview of Trigger Behavior</title>
 
    <para>

index a4003cf59e1..c0bec014154 100644
 
 		if (att->attisdropped)
 			continue;

 			/*
 
 		/*
index 7900a8f6a13..60e77753b94 100644
 
 	foreach(lc, columns)
 	{

index 59c836fc24d..b9220c54388 100644
 
 	/*

index b49d9ab78bf..0f57e654a79 100644
@@ -689,6 +690,8 @@ TransformPubWhereClauses(List *tables, const char *queryString,
 
+		whereclause = expand_generated_columns_in_expr(whereclause, pri->relation, 1);
+
 		/*

 
 			Assert(expr != NULL);

 			k = -1;

 			/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d617c4bc63d..047ba3a66d4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -3039,6 +3039,15 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 									 errhint("A child table column cannot be generated unless its parent column is.")));
 					}
 
@@ -3324,6 +3333,15 @@ MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const
 
@@ -6130,7 +6148,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
@@ -7308,7 +7326,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,

@@ -7785,6 +7803,14 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
@@ -8411,6 +8437,8 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
@@ -8425,36 +8453,70 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,

 
 
 	/*
 
 	/*
 
 	/*
@@ -8483,7 +8545,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 
@@ -8492,16 +8554,19 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,

 
 
 	/* Drop any pg_statistic entry for the column */
 	RemoveStatistics(RelationGetRelid(rel), attnum);
@@ -8590,17 +8655,30 @@ ATExecDropExpression(Relation rel, const char *colName, bool missing_ok, LOCKMOD
 				 errmsg("cannot alter system column \"%s\"",
 						colName)));
 
@@ -8743,6 +8821,16 @@ ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa
 
+	/*
@@ -9925,6 +10013,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -12305,7 +12406,7 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 	val = SysCacheGetAttrNotNull(CONSTROID, contuple,
 								 Anum_pg_constraint_conbin);
 	conbin = TextDatumGetCString(val);
-	newcon->qual = (Node *) stringToNode(conbin);
+	newcon->qual = expand_generated_columns_in_expr(stringToNode(conbin), rel, 1);
 
 	/* Find or create work queue entry for this table */
 	tab = ATGetQueueEntry(wqueue, rel);
@@ -13483,8 +13584,12 @@ ATPrepAlterColumnType(List **wqueue,
 					   list_make1_oid(rel->rd_rel->reltype),
 					   0);
 
@@ -13557,11 +13662,12 @@ ATPrepAlterColumnType(List **wqueue,
 
@@ -16550,6 +16656,14 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 
@@ -18797,8 +18911,11 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 
 			/*
@@ -18880,9 +18997,12 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 				}
 
 				/*
index 7a5ffe32f60..97c087929f3 100644

 /*
 
 			/*
 
 			/*
 
+			tgqual = expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc, PRS2_OLD_VARNO);
+			tgqual = expand_generated_columns_in_expr(tgqual, relinfo->ri_RelationDesc, PRS2_NEW_VARNO);
@@ -6621,3 +6630,37 @@ pg_trigger_depth(PG_FUNCTION_ARGS)
index 1127e6f11eb..c62a4a38cd8 100644
@@ -2335,6 +2335,10 @@ CheckVarSlotCompatibility(TupleTableSlot *slot, int attnum, Oid vartype)

+		/* Internal error: somebody forgot to expand it. */
+		if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			elog(ERROR, "unexpected virtual generated column reference");
+
 		if (attr->attisdropped)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index fb8dba3ab2c..bab5dfe47a0 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1762,6 +1762,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
 				continue;
 
@@ -2309,7 +2310,9 @@ ExecBuildSlotValueDescription(Oid reloid,
 
index 7c539de5cf2..7097ad15a9a 100644
@@ -1382,8 +1382,8 @@ Bitmapset *
 
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index bc82e035ba2..2d490cf7ca5 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -391,11 +391,14 @@ ExecCheckTIDVisible(EState *estate,
 }
 
 /*
@@ -403,9 +406,9 @@ ExecCheckTIDVisible(EState *estate,
@@ -416,7 +419,7 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,

 	/*
@@ -442,7 +445,9 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,

@@ -467,8 +472,11 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
 			}

 			/* If UPDATE, mark column in resultRelInfo->ri_extraUpdatedCols */
 			if (cmdtype == CMD_UPDATE)
@@ -478,6 +486,13 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,
 		}
 	}
 
@@ -496,6 +511,8 @@ ExecInitStoredGenerated(ResultRelInfo *resultRelInfo,

@@ -526,7 +543,7 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
@@ -534,7 +551,7 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
index d7f9c00c409..d3887628d46 100644
@@ -636,7 +636,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

 
@@ -3995,7 +3995,7 @@ ColConstraintElem:
 					n->location = @1;
 					$$ = (Node *) n;
 				}
-			| GENERATED generated_when AS '(' a_expr ')' STORED
+			| GENERATED generated_when AS '(' a_expr ')' opt_virtual_or_stored
 				{
 					Constraint *n = makeNode(Constraint);
 
@@ -4003,6 +4003,7 @@ ColConstraintElem:
 					n->generated_when = $2;
 					n->raw_expr = $5;
 					n->cooked_expr = NULL;
+					n->generated_kind = $7;
 					n->location = @1;
 
 					/*
@@ -4050,6 +4051,12 @@ generated_when:
 			| BY DEFAULT	{ $$ = ATTRIBUTE_IDENTITY_BY_DEFAULT; }
 		;
 
+opt_virtual_or_stored:
+			STORED			{ $$ = ATTRIBUTE_GENERATED_STORED; }
+			| VIRTUAL		{ $$ = ATTRIBUTE_GENERATED_VIRTUAL; }
+			| /*EMPTY*/		{ $$ = ATTRIBUTE_GENERATED_VIRTUAL; }
+		;
+
 /*
  * ConstraintAttr represents constraint attributes, which we parse as if
  * they were independent constraint clauses, in order to avoid shift/reduce
@@ -17990,6 +17997,7 @@ unreserved_keyword:
 			| VERSION_P
 			| VIEW
 			| VIEWS
+			| VIRTUAL
 			| VOLATILE
 			| WHITESPACE_P
 			| WITHIN
@@ -18645,6 +18653,7 @@ bare_label_keyword:
 			| VERSION_P
 			| VIEW
 			| VIEWS
+			| VIRTUAL
 			| VOLATILE
 			| WHEN
 			| WHITESPACE_P
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 101fba34b18..04ecf64b1fc 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -712,7 +712,11 @@ scanNSItemForColumn(ParseState *pstate, ParseNamespaceItem *nsitem,
 						colname),
 				 parser_errposition(pstate, location)));

 	/*
index a363c88ffc0..a392b93a179 100644
@@ -1009,7 +1010,7 @@ pgoutput_row_filter_init(PGOutputData *data, List *publications,
 				continue;

 			/* combine the row filter and cache the ExprState */
 			rfnode = make_orclause(filters);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index b74f2acc327..8c998176488 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -96,6 +96,8 @@ static List *matchLocks(CmdType event, Relation relation,
 						int varno, Query *parsetree, bool *hasUpdate);
 static Query *fireRIRrules(Query *parsetree, List *activeRIRs);
 static Bitmapset *adjust_view_column_set(Bitmapset *cols, List *targetlist);
+static Node *expand_generated_columns_internal(Node *node, Relation rel, int rt_index,
+											   RangeTblEntry *rte, int result_relation);

 /*
@@ -986,7 +988,8 @@ rewriteTargetListIU(List *targetList,
 		if (att_tup->attgenerated)
 		{
 			/*
-			 * stored generated column will be fixed in executor
+			 * virtual generated column stores a null value; stored generated
+			 * column will be fixed in executor
 			 */
 			new_tle = NULL;
 		}
@@ -2209,6 +2212,10 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 	 * requires special recursion detection if the new quals have sublink
 	 * subqueries, and if we did it in the loop above query_tree_walker would
 	 * then recurse into those quals a second time.
+	 *
+	 * Finally, we expand any virtual generated columns.  We do this after
+	 * each table's RLS policies are applied because the RLS policies might
+	 * also refer to the table's virtual generated columns.
 	 */
 	rt_index = 0;
 	foreach(lc, parsetree->rtable)
@@ -2222,10 +2229,11 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 
 		++rt_index;

 		rel = table_open(rte->relid, NoLock);
@@ -2314,6 +2322,16 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		if (hasSubLinks)
 			parsetree->hasSubLinks = true;
 
+		/*
+		 * Expand any references to virtual generated columns of this table.
+		 * Note that subqueries in virtual generated column expressions are
+		 * not currently supported, so this cannot add any more sublinks.
+		 */
+		parsetree = (Query *)
+			expand_generated_columns_internal((Node *) parsetree,
+											  rel, rt_index, rte,
+											  parsetree->resultRelation);
+
 		table_close(rel, NoLock);
 	}
 
@@ -4428,6 +4446,111 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
 }

+/*
+ * Expand virtual generated columns
+ *
+ * If the table contains virtual generated columns, build a target list
+ * containing the expanded expressions and use ReplaceVarsFromTargetList() to
+ * do the replacements.
+ *
+ * Vars matching rt_index at the current query level are replaced by the
+ * virtual generated column expressions from rel, if there are any.
+ *
+ * The caller must also provide rte, the RTE describing the target relation,
+ * in order to handle any whole-row Vars referencing the target, and
+ * result_relation, the index of the result relation, if this is part of an
+ * INSERT/UPDATE/DELETE/MERGE query.
+ */
+static Node *
+expand_generated_columns_internal(Node *node, Relation rel, int rt_index,
+								  RangeTblEntry *rte, int result_relation)
+{
+	TupleDesc	tupdesc;
+
+	tupdesc = RelationGetDescr(rel);
+	if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
+	{
+		List	   *tlist = NIL;
+
+		for (int i = 0; i < tupdesc->natts; i++)
+		{
+			Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+
+			if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			{
+				Node	   *defexpr;
+				int			attnum = i + 1;
+				Oid			attcollid;
+				TargetEntry *te;
+
+				defexpr = build_column_default(rel, attnum);
+				if (defexpr == NULL)
+					elog(ERROR, "no generation expression found for column number %d of table \"%s\"",
+						 attnum, RelationGetRelationName(rel));
+
+				/*
+				 * If the column definition has a collation and it is
+				 * different from the collation of the generation expression,
+				 * put a COLLATE clause around the expression.
+				 */
+				attcollid = attr->attcollation;
+				if (attcollid && attcollid != exprCollation(defexpr))
+				{
+					CollateExpr *ce = makeNode(CollateExpr);
+
+					ce->arg = (Expr *) defexpr;
+					ce->collOid = attcollid;
+					ce->location = -1;
+
+					defexpr = (Node *) ce;
+				}
+
+				ChangeVarNodes(defexpr, 1, rt_index, 0);
+
+				te = makeTargetEntry((Expr *) defexpr, attnum, 0, false);
+				tlist = lappend(tlist, te);
+			}
+		}
+
+		Assert(list_length(tlist) > 0);
+
+		node = ReplaceVarsFromTargetList(node, rt_index, 0, rte, tlist,
+										 result_relation,
+										 REPLACEVARS_CHANGE_VARNO, rt_index,
+										 NULL);
+	}
+
+	return node;
+}
+
+/*
+ * Expand virtual generated columns in an expression
+ *
+ * This is for expressions that are not part of a query, such as default
+ * expressions or index predicates.  The rt_index is usually 1.
+ */
+Node *
+expand_generated_columns_in_expr(Node *node, Relation rel, int rt_index)
+{
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+
+	if (tupdesc->constr && tupdesc->constr->has_generated_virtual)
+	{
+		RangeTblEntry *rte;
+
+		rte = makeNode(RangeTblEntry);
+		/* eref needs to be set, but the actual name doesn't matter */
+		rte->eref = makeAlias(RelationGetRelationName(rel), NIL);
+		rte->rtekind = RTE_RELATION;
+		rte->relid = RelationGetRelid(rel);
+
+		node = expand_generated_columns_internal(node, rel, rt_index, rte, 0);
+	}
+
+	return node;
+}
+
+
 /*
  * QueryRewrite -
  *	  Primary entry point to the query rewriter.
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index ee39d085ebe..9e9f5c621c8 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -592,6 +592,8 @@ RelationBuildTupleDesc(Relation relation)
 			constr->has_not_null = true;
 		if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
 			constr->has_generated_stored = true;
+		if (attp->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			constr->has_generated_virtual = true;
 		if (attp->atthasdef)
 			ndef++;
 
index af857f00c7c..1a662f387c8 100644
@@ -16190,6 +16190,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
index 805ba9f49fd..bc5d9222a20 100644
@@ -3706,12 +3706,14 @@
index 2e84b61f184..b342d91546f 100644
@@ -2123,6 +2123,12 @@ describeOneTableDetails(const char *schemaname,

 /*
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index e3a308024de..8b9a8453f77 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202501232
+#define CATALOG_VERSION_NO	202501271
 
 #endif
 
index b33c315d233..deaa515fe53 100644

 

 
 extern void ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
 									   EState *estate, TupleTableSlot *slot,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index d0f2dca5928..f4ddba00975 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -556,6 +556,9 @@ typedef struct ResultRelInfo
 	int			ri_NumGeneratedNeededI;
 	int			ri_NumGeneratedNeededU;

index ffe155ee20e..8dd421fa0ef 100644
@@ -2812,6 +2812,7 @@ typedef struct Constraint

 

 

 

 

 
index 910de9120f2..96a134d1561 100644
@@ -2555,6 +2555,26 @@ DROP TABLE pagg_tab6;

 

 
 
index 7653326420e..3ce0dd1831c 100644
@@ -1,3 +1,4 @@
@@ -357,6 +358,10 @@ ERROR:  column "b" inherits from generated column but specifies default
@@ -868,6 +873,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -881,6 +891,11 @@ ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09
@@ -1188,9 +1203,9 @@ SELECT * FROM gtest29;
@@ -1471,7 +1486,7 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
 
similarity index 72%
index 7653326420e..35638812be9 100644
@@ -1,9 +1,10 @@
@@ -12,89 +13,89 @@ SELECT table_name, column_name, column_default, is_nullable, is_generated, gener

 \d gtest1
 
 -- duplicate generated
@@ -165,16 +166,10 @@ SELECT a, b FROM gtest1 WHERE b = 4 ORDER BY a;
  2 | 4
 (1 row)
 
--- test that overflow error happens on write
+-- test that overflow error happens on read
 INSERT INTO gtest1 VALUES (2000000000);
-ERROR:  integer out of range
 SELECT * FROM gtest1;
- a | b 
----+---
- 2 | 4
- 1 | 2
-(2 rows)
-
+ERROR:  integer out of range
 DELETE FROM gtest1 WHERE a = 2000000000;
 -- test with joins
 CREATE TABLE gtestx (x int, y int);
@@ -220,8 +215,8 @@ CREATE TABLE gtestm (
   id int PRIMARY KEY,
   f1 int,
   f2 int,
-  f3 int GENERATED ALWAYS AS (f1 * 2) STORED,
-  f4 int GENERATED ALWAYS AS (f2 * 2) STORED
+  f3 int GENERATED ALWAYS AS (f1 * 2) VIRTUAL,
+  f4 int GENERATED ALWAYS AS (f2 * 2) VIRTUAL
 );
 INSERT INTO gtestm VALUES (1, 5, 100);
 MERGE INTO gtestm t USING (VALUES (1, 10), (2, 20)) v(id, f1) ON t.id = v.id
@@ -244,7 +239,7 @@ SELECT * FROM gtestm ORDER BY id;
 DROP TABLE gtestm;
 CREATE TABLE gtestm (
   a int PRIMARY KEY,
-  b int GENERATED ALWAYS AS (a * 2) STORED
+  b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtestm (a) SELECT g FROM generate_series(1, 10) g;
 MERGE INTO gtestm t USING gtestm AS s ON 2 * t.a = s.b WHEN MATCHED THEN DELETE RETURNING *;
@@ -318,11 +313,11 @@ SELECT * FROM gtest1_1;
 (0 rows)
 
 \d gtest1_1
 
 INSERT INTO gtest1_1 VALUES (4);
@@ -341,12 +336,12 @@ SELECT * FROM gtest1;
 
@@ -357,26 +352,30 @@ ERROR:  column "b" inherits from generated column but specifies default
 
 INSERT INTO gtestx (a, x) VALUES (11, 22);
 SELECT * FROM gtest1;
- a  |  b  
-----+-----
-  3 |   6
-  4 |   8
- 11 | 242
+ a  | b  
+----+----
+  3 |  6
+  4 |  8
+ 11 | 22
 (3 rows)
 
 SELECT * FROM gtestx;
@@ -388,9 +387,9 @@ SELECT * FROM gtestx;
@@ -403,28 +402,28 @@ CREATE TABLE gtest1_y () INHERITS (gtest1, gtesty);  -- error
 
@@ -441,8 +440,8 @@ TABLE gtestc;
 
+-- test update
@@ -463,7 +462,7 @@ SELECT * FROM gtest3 ORDER BY a;
     |   
 (4 rows)
 
@@ -528,7 +527,7 @@ SELECT * FROM gtest3 ORDER BY a;
 (4 rows)
 
 -- null values
-CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED);
+CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) VIRTUAL);
 INSERT INTO gtest2 VALUES (1);
 SELECT * FROM gtest2;
  a | b 
@@ -537,7 +536,7 @@ SELECT * FROM gtest2;
 (1 row)
 
@@ -552,7 +551,7 @@ DROP TABLE gtest_varlena;
@@ -567,11 +566,11 @@ DROP TYPE double_int;
@@ -580,7 +579,7 @@ SELECT * FROM gtest_tableoid;
 (2 rows)
 
 -- drop column behavior
@@ -588,24 +587,24 @@ HINT:  Use DROP ... CASCADE to drop the dependent objects too.
 
-CREATE TABLE gtest10a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE gtest10a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 ALTER TABLE gtest10a DROP COLUMN b;
 INSERT INTO gtest10a (a) VALUES (1);
 -- privileges
 CREATE USER regress_user11;
-CREATE TABLE gtest11 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) STORED);
+CREATE TABLE gtest11 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) VIRTUAL);
 INSERT INTO gtest11 VALUES (1, 10), (2, 20);
 GRANT SELECT (a, c) ON gtest11 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 gtest12 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) STORED);
+CREATE TABLE gtest12 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) VIRTUAL);
 INSERT INTO gtest12 VALUES (1, 10), (2, 20);
 GRANT SELECT (a, c), INSERT ON gtest12 TO regress_user11;
 SET ROLE regress_user11;
@@ -620,15 +619,9 @@ SELECT a, c FROM gtest11;  -- allowed
 
-INSERT INTO gtest12 VALUES (3, 30), (4, 40);  -- currently not allowed because of function permissions, should arguably be allowed
+INSERT INTO gtest12 VALUES (3, 30), (4, 40);  -- allowed (does not actually invoke the function)
+SELECT a, c FROM gtest12;  -- currently not allowed because of function permissions, should arguably be allowed
-SELECT a, c FROM gtest12;  -- allowed (does not actually invoke the function)
@@ -638,227 +631,147 @@ DROP TABLE gtest11, gtest12;
 -- check with whole-row reference
-CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
 ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL);
 INSERT INTO gtest20c VALUES (1);  -- ok
 INSERT INTO gtest20c VALUES (NULL);  -- fails
 ERROR:  new row for relation "gtest20c" violates check constraint "whole_row_check"
-DETAIL:  Failing row contains (null, null).
 CREATE TYPE gtestdomain1range AS range (subtype = gtestdomain1);
-CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) STORED);
-INSERT INTO gtest24r (a) VALUES (4);  -- ok
-INSERT INTO gtest24r (a) VALUES (6);  -- error
@@ -868,6 +781,11 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -882,32 +800,37 @@ ERROR:  table "gtest_child3" being attached contains an identity column "f3"
 
 \d gtest_child2
 
 \d gtest_child3
 
@@ -918,7 +841,7 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
 
 SELECT tableoid::regclass, * FROM gtest_child ORDER BY 1, 2, 3;
@@ -928,7 +851,7 @@ SELECT tableoid::regclass, * FROM gtest_child ORDER BY 1, 2, 3;
  gtest_child | 07-15-2016 |  2 |  4
 (2 rows)
 
-SELECT tableoid::regclass, * FROM gtest_child2 ORDER BY 1, 2, 3;
+SELECT tableoid::regclass, * FROM gtest_child2 ORDER BY 1, 2, 3;  -- uses child's generation expression, not parent's
    tableoid   |     f1     | f2 | f3 
 --------------+------------+----+----
  gtest_child2 | 08-15-2016 |  3 | 66
@@ -944,95 +867,95 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
    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
+ gtest_child2 | 08-15-2016 |  3 |  6
+ gtest_child3 | 09-13-2016 |  1 |  2
 (3 rows)

 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3

 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3
 
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
@@ -1045,20 +968,20 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
 
@@ -1066,16 +989,16 @@ SELECT * FROM gtest25 ORDER BY a;
  4 | 12
 (2 rows)
 
@@ -1084,15 +1007,15 @@ SELECT * FROM gtest25 ORDER BY a;
 (2 rows)
 
 \d gtest25
 
@@ -1100,7 +1023,7 @@ Indexes:
 CREATE TABLE gtest27 (
     a int,
     b int,
@@ -1108,12 +1031,12 @@ ERROR:  cannot alter type of a column used by a generated column
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1129,20 +1052,19 @@ LINE 1: ALTER TABLE gtest27 ALTER COLUMN x TYPE boolean USING x <> 0...
 
@@ -1151,12 +1073,12 @@ ALTER TABLE gtest27
 
 SELECT * FROM gtest27;
  a | b  | x  
@@ -1168,7 +1090,7 @@ SELECT * FROM gtest27;
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -1179,18 +1101,18 @@ SELECT * FROM gtest29;
 (2 rows)
 
 \d gtest29
 
@@ -1201,97 +1123,97 @@ SELECT * FROM gtest29;
 (2 rows)
 
 \d gtest29

 \d gtest29

 \d gtest30_1

 \d gtest30_1
 
 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION;  -- error
 ERROR:  cannot drop generation expression from inherited column
 -- composite type dependencies
-CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text);
+CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') VIRTUAL, c text);
 CREATE TABLE gtest31_2 (x int, y gtest31_1);
 ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
 ERROR:  cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type
 DROP TABLE gtest31_1, gtest31_2;
 -- Check it for a partitioned table, too
-CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text) PARTITION BY LIST (a);
+CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') VIRTUAL, c text) PARTITION BY LIST (a);
 CREATE TABLE gtest31_2 (x int, y gtest31_1);
 ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
 ERROR:  cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type
@@ -1299,7 +1221,7 @@ DROP TABLE gtest31_1, gtest31_2;
 -- triggers
 CREATE TABLE gtest26 (
     a int PRIMARY KEY,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE FUNCTION gtest_trigger_func() RETURNS trigger
   LANGUAGE plpgsql
@@ -1352,7 +1274,7 @@ CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26
   EXECUTE PROCEDURE gtest_trigger_func();
 INSERT INTO gtest26 (a) VALUES (-2), (0), (3);
 INFO:  gtest2: BEFORE: new = (-2,)
-INFO:  gtest4: AFTER: new = (-2,-4)
+INFO:  gtest4: AFTER: new = (-2,)
 SELECT * FROM gtest26 ORDER BY a;
  a  | b  
 ----+----
@@ -1362,12 +1284,12 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
@@ -1377,8 +1299,8 @@ SELECT * FROM gtest26 ORDER BY a;
 (3 rows)
 
 DELETE FROM gtest26 WHERE a = -6;
-INFO:  gtest1: BEFORE: old = (-6,-12)
-INFO:  gtest3: AFTER: old = (-6,-12)
+INFO:  gtest1: BEFORE: old = (-6,)
+INFO:  gtest3: AFTER: old = (-6,)
 SELECT * FROM gtest26 ORDER BY a;
  a | b 
 ---+---
@@ -1428,7 +1350,7 @@ CREATE TRIGGER gtest12_03 BEFORE INSERT OR UPDATE ON gtest26
   EXECUTE PROCEDURE gtest_trigger_func();
 INSERT INTO gtest26 (a) VALUES (1);
 INFO:  gtest12_01: BEFORE: new = (1,)
-INFO:  gtest12_03: BEFORE: new = (10,300)
+INFO:  gtest12_03: BEFORE: new = (10,)
 SELECT * FROM gtest26 ORDER BY a;
  a  | b  
 ----+----
@@ -1436,9 +1358,9 @@ SELECT * FROM gtest26 ORDER BY a;
 (1 row)
 
 UPDATE gtest26 SET a = 11 WHERE a = 10;
-INFO:  gtest12_01: BEFORE: old = (10,20)
-INFO:  gtest12_03: BEFORE: old = (10,20)
@@ -1451,27 +1373,27 @@ CREATE TABLE gtest28a (

index bc3898fbe58..99e98bcbdce 100644
 
@@ -711,7 +727,9 @@ CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
@@ -748,9 +766,12 @@ UPDATE testpub_tbl5 SET a = 1;
index 1edd9e45ebb..e63ee2cf2bb 100644
@@ -78,7 +78,7 @@ test: brin_bloom brin_multi
 # psql depends on create_am
 # amutils depends on geometry, create_index_spgist, hash_index, brin
 # ----------
-test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions sysviews tsrf tid tidscan tidrangescan collate.utf8 collate.icu.utf8 incremental_sort create_role without_overlaps
+test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions sysviews tsrf tid tidscan tidrangescan collate.utf8 collate.icu.utf8 incremental_sort create_role without_overlaps generated_virtual
 
 # collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
 test: rules psql psql_crosstab amutils stats_ext collate.linux.utf8 collate.windows.win1252
diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql
index f99f186f2d6..eea50e34c2d 100644
--- a/src/test/regress/sql/collate.icu.utf8.sql
+++ b/src/test/regress/sql/collate.icu.utf8.sql
@@ -955,6 +955,21 @@ CREATE TABLE pagg_tab6_p2 PARTITION OF pagg_tab6 FOR VALUES IN ('c', 'd');
 RESET max_parallel_workers_per_gather;
 RESET enable_incremental_sort;

 

index 6fbfcbf9615..b7749ce355f 100644
@@ -1,3 +1,6 @@
@@ -149,6 +152,7 @@ CREATE TABLE gtest_normal_child (a int, b int GENERATED ALWAYS AS (a * 2) STORED
 INSERT INTO gtestx (a, x) VALUES (11, 22);
@@ -438,6 +442,9 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
@@ -447,6 +454,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint DEFAULT 42);
@@ -715,4 +725,4 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);

similarity index 74%
index 6fbfcbf9615..34870813910 100644
@@ -1,51 +1,54 @@

 
 
 \d gtest1
 
 -- duplicate generated

 -- invalid reference

 

 
@@ -68,7 +71,7 @@ CREATE TABLE gtest_err_8 (a int PRIMARY KEY, b int GENERATED BY DEFAULT AS (a *
 
@@ -91,8 +94,8 @@ CREATE TABLE gtestm (
@@ -104,7 +107,7 @@ CREATE TABLE gtestm (
 
 CREATE TABLE gtestm (
   a int PRIMARY KEY,
-  b int GENERATED ALWAYS AS (a * 2) STORED
+  b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtestm (a) SELECT g FROM generate_series(1, 10) g;
 MERGE INTO gtestm t USING gtestm AS s ON 2 * t.a = s.b WHEN MATCHED THEN DELETE RETURNING *;
@@ -141,15 +144,16 @@ CREATE TABLE gtest1_1 () INHERITS (gtest1);

 -- test inheritance mismatches between parent and child
 CREATE TABLE gtestx (x int, b int DEFAULT 10) INHERITS (gtest1);  -- error
 CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS IDENTITY) INHERITS (gtest1);  -- error
-CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) STORED) INHERITS (gtest1);  -- ok, overrides parent
+CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) STORED) INHERITS (gtest1);  -- error
+CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) VIRTUAL) INHERITS (gtest1);  -- ok, overrides parent
 \d+ gtestx
 INSERT INTO gtestx (a, x) VALUES (11, 22);
 SELECT * FROM gtest1;
@@ -157,9 +161,9 @@ CREATE TABLE gtestx (x int, b int GENERATED ALWAYS AS (a * 22) STORED) INHERITS

 -- test multiple inheritance mismatches
@@ -171,28 +175,28 @@ CREATE TABLE gtesty (x int, b int);
 CREATE TABLE gtest1_y () INHERITS (gtest1, gtesty);  -- error
 DROP TABLE gtesty;

 
--- test stored update
+-- test update
 
@@ -232,12 +236,12 @@ CREATE TABLE gtest3a (a text, b text GENERATED ALWAYS AS (a || '+' || a) STORED)
 
 -- null values
 
@@ -247,7 +251,7 @@ CREATE TABLE gtest_varlena (a varchar, b varchar GENERATED ALWAYS AS (a) STORED)
@@ -258,35 +262,35 @@ CREATE TABLE gtest4 (
 
 -- drop column behavior
 
 \d gtest10

 -- privileges
 
-CREATE TABLE gtest11 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) STORED);
+CREATE TABLE gtest11 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) VIRTUAL);
 INSERT INTO gtest11 VALUES (1, 10), (2, 20);
 GRANT SELECT (a, c) ON gtest11 TO regress_user11;

-CREATE TABLE gtest12 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) STORED);
+CREATE TABLE gtest12 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) VIRTUAL);
 INSERT INTO gtest12 VALUES (1, 10), (2, 20);
 GRANT SELECT (a, c), INSERT ON gtest12 TO regress_user11;
 
@@ -294,8 +298,8 @@ CREATE TABLE gtest12 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b
 SELECT a, b FROM gtest11;  -- not allowed
 SELECT a, c FROM gtest11;  -- allowed
 SELECT gf1(10);  -- not allowed
-INSERT INTO gtest12 VALUES (3, 30), (4, 40);  -- currently not allowed because of function permissions, should arguably be allowed
-SELECT a, c FROM gtest12;  -- allowed (does not actually invoke the function)
+INSERT INTO gtest12 VALUES (3, 30), (4, 40);  -- allowed (does not actually invoke the function)
+SELECT a, c FROM gtest12;  -- currently not allowed because of function permissions, should arguably be allowed
 RESET ROLE;
 
 DROP FUNCTION gf1(int);  -- fail
@@ -304,133 +308,139 @@ CREATE TABLE gtest12 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b
 DROP USER regress_user11;
 
 -- check constraints

 
 
 -- check with whole-row reference
-CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL);
 INSERT INTO gtest20c VALUES (1);  -- ok
 INSERT INTO gtest20c VALUES (NULL);  -- fails
 
--- not-null constraints

 -- index constraints
 
 -- indexes
 
 -- foreign keys

 
 
-DROP TABLE gtest23b;
-DROP TABLE gtest23a;
+--DROP TABLE gtest23b;
+--DROP TABLE gtest23a;

 
 -- 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
+CREATE TABLE gtest24 (a int PRIMARY KEY, b gtestdomain1 GENERATED ALWAYS AS (a * 2) VIRTUAL);
+--INSERT INTO gtest24 (a) VALUES (4);  -- ok
+--INSERT INTO gtest24 (a) VALUES (6);  -- error
 CREATE TYPE gtestdomain1range AS range (subtype = gtestdomain1);
-CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) STORED);
-INSERT INTO gtest24r (a) VALUES (4);  -- ok
-INSERT INTO gtest24r (a) VALUES (6);  -- error
+CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS AS (gtestdomain1range(a, a + 5)) VIRTUAL);
+--INSERT INTO gtest24r (a) VALUES (4);  -- ok
+--INSERT INTO gtest24r (a) VALUES (6);  -- error

 -- partitioning cases
 
-CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f1);
+CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE (f1);
 CREATE TABLE gtest_child PARTITION OF gtest_parent
   FOR VALUES FROM ('2016-07-01') TO ('2016-08-01');  -- inherits gen expr
 CREATE TABLE gtest_child2 PARTITION OF gtest_parent (
-    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 22) STORED  -- overrides gen expr
+    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 22) VIRTUAL  -- overrides gen expr
 ) FOR VALUES FROM ('2016-08-01') TO ('2016-09-01');
 CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
     f3 DEFAULT 42  -- error
@@ -438,6 +448,9 @@ CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
 CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
     f3 WITH OPTIONS GENERATED ALWAYS AS IDENTITY  -- error
 ) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
+CREATE TABLE gtest_child3 PARTITION OF gtest_parent (
+    f3 GENERATED ALWAYS AS (f2 * 2) STORED  -- error
+) FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
 DROP TABLE gtest_child3;
@@ -448,6 +461,9 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
 DROP TABLE gtest_child3;
 CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) STORED);
+ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01'); -- error
+DROP TABLE gtest_child3;
+CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 33) VIRTUAL);
 ALTER TABLE gtest_parent ATTACH PARTITION gtest_child3 FOR VALUES FROM ('2016-09-01') TO ('2016-10-01');
 \d gtest_child
 \d gtest_child2
@@ -457,7 +473,7 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 INSERT INTO gtest_parent (f1, f2) VALUES ('2016-08-15', 3);
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
 SELECT tableoid::regclass, * FROM gtest_child ORDER BY 1, 2, 3;
-SELECT tableoid::regclass, * FROM gtest_child2 ORDER BY 1, 2, 3;
+SELECT tableoid::regclass, * FROM gtest_child2 ORDER BY 1, 2, 3;  -- uses child's generation expression, not parent's
 SELECT tableoid::regclass, * FROM gtest_child3 ORDER BY 1, 2, 3;
 UPDATE gtest_parent SET f1 = f1 + 60 WHERE f2 = 1;
 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3;
@@ -481,21 +497,21 @@ CREATE TABLE gtest_child3 (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWA
 -- we leave these tables around for purposes of testing dump/reload/upgrade

 
@@ -503,7 +519,7 @@ CREATE TABLE gtest25 (a int PRIMARY KEY);
 CREATE TABLE gtest27 (
     a int,
     b int,
-    x int GENERATED ALWAYS AS ((a + b) * 2) STORED
+    x int GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL
 );
 INSERT INTO gtest27 (a, b) VALUES (3, 7), (4, 11);
 ALTER TABLE gtest27 ALTER COLUMN a TYPE text;  -- error
@@ -517,7 +533,7 @@ CREATE TABLE gtest27 (
   DROP COLUMN x,
   ALTER COLUMN a TYPE bigint,
   ALTER COLUMN b TYPE bigint,
-  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) STORED;
+  ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) VIRTUAL;
 \d gtest27
 -- Ideally you could just do this, but not today (and should x change type?):
 ALTER TABLE gtest27
@@ -529,7 +545,7 @@ CREATE TABLE gtest27 (
 -- ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
 CREATE TABLE gtest29 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 INSERT INTO gtest29 (a) VALUES (3), (4);
 SELECT * FROM gtest29;
@@ -543,20 +559,20 @@ CREATE TABLE gtest29 (
 SELECT * FROM gtest29;
 \d gtest29

 
 -- with inheritance
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE gtest30 ALTER COLUMN b DROP EXPRESSION;
@@ -565,7 +581,7 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 DROP TABLE gtest30 CASCADE;
 CREATE TABLE gtest30 (
     a int,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE ONLY gtest30 ALTER COLUMN b DROP EXPRESSION;  -- error
@@ -574,13 +590,13 @@ CREATE TABLE gtest30_1 () INHERITS (gtest30);
 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION;  -- error
 
 -- composite type dependencies
-CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text);
+CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') VIRTUAL, c text);
 CREATE TABLE gtest31_2 (x int, y gtest31_1);
 ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
 DROP TABLE gtest31_1, gtest31_2;
 
 -- Check it for a partitioned table, too
-CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text) PARTITION BY LIST (a);
+CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') VIRTUAL, c text) PARTITION BY LIST (a);
 CREATE TABLE gtest31_2 (x int, y gtest31_1);
 ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar;  -- fails
 DROP TABLE gtest31_1, gtest31_2;
@@ -588,7 +604,7 @@ CREATE TABLE gtest31_2 (x int, y gtest31_1);
 -- triggers
 CREATE TABLE gtest26 (
     a int PRIMARY KEY,
-    b int GENERATED ALWAYS AS (a * 2) STORED
+    b int GENERATED ALWAYS AS (a * 2) VIRTUAL
 );
 
 CREATE FUNCTION gtest_trigger_func() RETURNS trigger
@@ -704,7 +720,7 @@ CREATE TABLE gtest28a (
   a int,
   b int,
   c int,
-  x int GENERATED ALWAYS AS (b * 2) STORED
+  x int GENERATED ALWAYS AS (b * 2) VIRTUAL
 );
 
 ALTER TABLE gtest28a DROP COLUMN a;
@@ -715,4 +731,4 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);

index 47f0329c244..22ffb86747e 100644
 
@@ -435,7 +447,9 @@ CREATE PUBLICATION testpub_fortable FOR TABLE testpub_tbl1;
@@ -462,9 +476,11 @@ CREATE UNIQUE INDEX testpub_tbl5_b_key ON testpub_tbl5 (b, c);

 
 DROP TABLE r1;
 
+--
 
index 5970bb47360..c7a4c52e4f2 100644

 
 # data for initial sync
@@ -42,10 +42,11 @@

 # data to replicate
 
@@ -56,11 +57,11 @@

 
@@ -69,7 +70,7 @@

 $result =
 
 # cleanup

 

 

@@ -725,6 +752,15 @@
 
index 7a535e76b08..e859bcdf4eb 100644
@@ -1209,7 +1209,7 @@
 # list) are considered to have the same column list.

base-commit: 65281391a937293db7fa747be218def0e9794550
-- 
2.48.1

---

From: Dean Rasheed <[email-masked]>
Date: Mon, 27 Jan 2025 12:42:18 +0000

> questions are resolved.  I think this is complete now.

In struct ResultRelInfo, the following field is added:

but that doesn't really seem to be accurate, because it's set to true
by ExecInitGenerated() whether it's called with CMD_INSERT or
CMD_UPDATE, so it will be true before both the other fields are
computed. It's used from ExecGetExtraUpdatedCols() as an indicator
that ri_extraUpdatedCols is valid, but it looks like that might not be
the case, if ExecInitGenerated() was only called with CMD_INSERT.

I'm not sure if that represents an actual bug, but it looks wrong. It
should perhaps be called "ri_extraUpdatedCols_valid", and only set to
true when ExecInitGenerated() is called with CMD_UPDATE, and
ri_extraUpdatedCols is populated.

Regards,
Dean

---

From: Shlok Kyal <[email-masked]>
Date: Tue, 28 Jan 2025 15:10:48 +0530

Hi,

I did some testing related to logical replication on the patch:

Test1: With row filter on publisher

-- publisher:
CREATE TABLE t1 (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
create publication pub1 for table t1 where (b > 50);
INSERT INTO t1 values(1);
INSERT INTO t1 values(32);

-- subscriber
CREATE TABLE t1 (a int, b int);
create subscription test1 connection 'dbname=postgres host=localhost
port=5432' publication pub1;
select * from t1;
a  | b
----+---
 32 |
(1 row)

Only records where b>50 are replicated to the subscriber.

--------------------------------------------------------------------------------------------------------------------

Test 2: Replication of virtual generated column using user defined operator

-- publisher
CREATE OPERATOR === (
    leftarg = integer,
    rightarg = integer,
    procedure = int4eq
);
CREATE TABLE t1 (a int, b bool GENERATED ALWAYS AS (a === 10)
VIRTUAL); INSERT INTO t1 values(1);
INSERT INTO t1 values(10);

-- create publication with row filter with user defined operator
create publication pub1 for table t1 where (a === 10);

ERROR:  invalid publication WHERE expression LINE 1: create
publication pub1 for table t1 where (a === 10)
                                                    ^
DETAIL:  User-defined operators are not allowed.

-- create publication on virtual generated column using user defined operator
create publication pub1 for table t1 where (b = 't');
ERROR:  invalid publication WHERE expression
DETAIL:  User-defined operators are not allowed.

----------------------------------------------------------------------------------------------------------------

Test 3: CREATE PUBLICATION on column list with Virtual generated column

CREATE TABLE t1 (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
create publication pub1 for table t1 (a, b);

It is failing with error:
ERROR:  cannot use virtual generated column "b" in publication column list.

----------------------------------------------------------------------------------------------------------------

Test 4: Update publication on non virtual gen

CREATE TABLE t1 (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
create publication pub1 for table t1 (a);
alter table t1 replica identity full;
update t1 set a = 10;

ERROR:  cannot update table "t1"
DETAIL:  Column list used by the publication does not cover the
replica identity.

----------------------------------------------------------------------------------------------------------------

Test 5: Update publication on non virtual gen with no column list specified

CREATE TABLE t1 (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
create publication pub1 for table t1;
alter table t1 replica identity full;
update t1 set a = 10;

No error is thrown, and an update is happening. It should have thrown
an ERROR as the unpublished generated column 'b' is part of the
replica identity.

Thanks and Regards,
Shlok Kyal

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 4 Feb 2025 23:36:09 +0100

> ri_extraUpdatedCols is populated.

Yeah, this is quite contorted.  I have renamed it like you suggested.
From 521b99cc1108cf9c4ad178af1d517722bf84e029 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Mon, 27 Jan 2025 07:42:55 +0100
Subject: [PATCH v14 1/3] Virtual generated columns

TODO: catversion
---
 70 files changed, 1529 insertions(+), 765 deletions(-)

 drop table test8;
 
index 7e487cff2a4..daa3b1d7a6d 100644

 
 select * from grem1;
- a  | b  
-----+----
-  1 |  2
- 22 | 44
+ a  | b  | c  
+----+----+----
+  1 |  2 |  3
+ 22 | 44 | 66
 (2 rows)
 
 delete from grem1;
 -- test copy from
 copy grem1 from stdin;
 select * from gloc1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 |  
+ 2 | 4 |  
 (2 rows)
 
 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;

 select * from grem1;
- a | b 
----+---
- 1 | 2
- 2 | 4
+ a | b | c 
+---+---+---
+ 1 | 2 | 3
+ 2 | 4 | 6
 (2 rows)
 
 delete from grem1;
index 088fb175cce..ee59a7e15d0 100644
 
index 7ff39ae8c67..ae156b6b1cd 100644
 
   <para>
 
   <para>
       See <xref linkend="logical-replication-gencols"/> for details.
 
+     <para>

    <varlistentry>
 
      <para>
 
      <para>

      <para>
 
      <para>
 
    <para>

    <para>

 
 		if (att->attisdropped)
 			continue;

 			/*
 
 		/*
index 41ffd494c81..d6f94db5d99 100644
 
 	foreach(lc, columns)
 	{
 
index f8da32e9aef..e5ab207d2ec 100644
 
index 5b1753d4681..f8d3ea820e1 100644
@@ -1110,6 +1110,9 @@ DefineIndex(Oid tableId,
@@ -1119,14 +1122,24 @@ DefineIndex(Oid tableId,
 
 	/*
 
@@ -1139,6 +1152,24 @@ DefineIndex(Oid tableId,
 
index 951ffabb656..801560c8fdc 100644

 
 
 			Assert(expr != NULL);

 			k = -1;

 			/*
index 18f64db6e39..94660e4bd45 100644

 

 

 
 	/*
 
 	/*
 
 	/*

 

 
 
+	/*
 
 	/*
@@ -12289,7 +12390,7 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 
 	/* Find or create work queue entry for this table */
 	tab = ATGetQueueEntry(wqueue, rel);
@@ -13467,8 +13568,12 @@ ATPrepAlterColumnType(List **wqueue,
 					   list_make1_oid(rel->rd_rel->reltype),
 					   0);
 
@@ -13541,11 +13646,12 @@ ATPrepAlterColumnType(List **wqueue,
 
@@ -16534,6 +16640,14 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 
@@ -18781,8 +18895,11 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 
 			/*
@@ -18864,9 +18981,12 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 				}
 
 				/*

 /*
 
 			/*
 
 			/*
 
index 09f6a5f14c1..1c3477b03c9 100644
@@ -2324,6 +2324,10 @@ CheckVarSlotCompatibility(TupleTableSlot *slot, int attnum, Oid vartype)

+		/* Internal error: somebody forgot to expand it. */
+		if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+			elog(ERROR, "unexpected virtual generated column reference");
+
 		if (attr->attisdropped)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_COLUMN),
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 604cb0625b8..044b4e9742a 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1775,6 +1775,7 @@ ExecRelCheck(ResultRelInfo *resultRelInfo,
 				continue;
 
@@ -2322,7 +2323,9 @@ ExecBuildSlotValueDescription(Oid reloid,
 
index 00564985668..123d3c79b43 100644
@@ -1383,8 +1383,8 @@ Bitmapset *

 /*

 	/*

 

 

 

 
 					/*
 
+opt_virtual_or_stored:

 	/*
index 0227fcbca3d..341e659d6ab 100644

index 847edcfa90e..e996bdc0d21 100644

 /*
@@ -2187,6 +2190,10 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
@@ -2200,10 +2207,11 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 
 		++rt_index;

 		rel = table_open(rte->relid, NoLock);
@@ -2292,6 +2300,16 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 		if (hasSubLinks)
 			parsetree->hasSubLinks = true;
 
+		/*
 
@@ -4406,6 +4424,111 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
 }

+/*
 
index 02e1fdf8f78..520e1338c28 100644
index aa4363b200a..69d82b44043 100644

 /*

 

 
 
index aca15f771a2..fe6070381f6 100644

 

 

 

 

 

 

 

 

 
 \d gtest1
 
 -- duplicate generated

 \d gtest1_1

 
 
 SELECT * FROM gtestx;

 
 
 -- null values

 -- drop column behavior

 
 \d gtest_child2
 
 \d gtest_child3

 

 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3

 
 \d gtest_child
 
 \d gtest_child2
 
 \d gtest_child3

 
 
 \d gtest25

 SELECT * FROM gtest27;
  a | b  | x  

 SELECT * FROM gtest27;
  a | b  | x  
 
 \d gtest29

 \d gtest29

 \d gtest29

 \d gtest30_1

 \d gtest30_1

 

 

 

 

 
 \d gtest1
 
 -- duplicate generated

 -- invalid reference

 

 

 CREATE TABLE gtestm (
   a int PRIMARY KEY,

 

 
 
--- test stored update

 -- null values

 -- drop column behavior
 
 \d gtest10

 -- privileges

 

 
 -- check constraints

 

--- not-null constraints

 -- index constraints
 
 -- indexes
 
 -- foreign keys

 
 
-DROP TABLE gtest23b;
-DROP TABLE gtest23a;
+--DROP TABLE gtest23b;
+--DROP TABLE gtest23a;

 
 -- domains

 -- partitioning cases

 

 
 
 -- with inheritance
 CREATE TABLE gtest30 (
     a int,

 

 

 
 DROP TABLE r1;
 
+--

 
 
 # data for initial sync
@@ -42,10 +42,11 @@

 # data to replicate
 
@@ -56,11 +57,11 @@

 
@@ -69,7 +70,7 @@

 $result =
 
 # cleanup

 

 

@@ -725,6 +752,15 @@

base-commit: 622f678c10202c8a0b350794d504eeef7b773e90
-- 
2.48.1

From 749b7adbf7bbfbf6428f99be57b7dce2f52f51db Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Tue, 4 Feb 2025 17:31:42 +0100
Subject: [PATCH v14 2/3] Fixup review Shlok Kyal 2025-01-28

---
 src/backend/commands/publicationcmds.c | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 801560c8fdc..06fb1823963 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -405,6 +405,14 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			relation->rd_att->constr->has_generated_stored)
 			*invalid_gen_col = true;
 
+		/*
+		 * Virtual generated columns are currently not supported for logical
+		 * replication at all.
+		 */
+		if (relation->rd_att->constr &&
+			relation->rd_att->constr->has_generated_virtual)
+			*invalid_gen_col = true;
+
 		if (*invalid_gen_col && *invalid_column_list)
 			return true;
 	}
@@ -431,7 +439,16 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 			 * The publish_generated_columns option must be set to stored if
 			 * the REPLICA IDENTITY contains any stored generated column.
 			 */
-			if (pubgencols_type != PUBLISH_GENCOLS_STORED && att->attgenerated)
+			if (att->attgenerated == ATTRIBUTE_GENERATED_STORED && pubgencols_type != PUBLISH_GENCOLS_STORED)
+			{
+				*invalid_gen_col = true;
+				break;
+			}
+			/*
+			 * The equivalent setting for virtual generated columns does not
+			 * exist yet.
+			 */
+			if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
 			{
 				*invalid_gen_col = true;
 				break;
-- 
2.48.1

From f1eaccf4eaafddedf2db16d6005f9ced705dd3ec Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Tue, 4 Feb 2025 23:35:22 +0100
Subject: [PATCH v14 3/3] Fixup review Dean Rasheed 2025-01-27

---
 src/backend/executor/execUtils.c       | 2 +-
 src/backend/executor/nodeModifyTable.c | 4 ++--
 src/include/nodes/execnodes.h          | 5 ++---
 3 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index 123d3c79b43..7eab229dde9 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -1383,7 +1383,7 @@ Bitmapset *
 ExecGetExtraUpdatedCols(ResultRelInfo *relinfo, EState *estate)
 {
 	/* Compute the info if we didn't already */
-	if (!relinfo->ri_Generated_valid)
+	if (!relinfo->ri_extraUpdatedCols_valid)
 		ExecInitGenerated(relinfo, estate, CMD_UPDATE);
 	return relinfo->ri_extraUpdatedCols;
 }
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 2d490cf7ca5..5bf522cac1a 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -501,6 +501,8 @@ ExecInitGenerated(ResultRelInfo *resultRelInfo,
 
 		resultRelInfo->ri_GeneratedExprsU = ri_GeneratedExprs;
 		resultRelInfo->ri_NumGeneratedNeededU = ri_NumGeneratedNeeded;
+
+		resultRelInfo->ri_extraUpdatedCols_valid = true;
 	}
 	else
 	{
@@ -511,8 +513,6 @@ ExecInitGenerated(ResultRelInfo *resultRelInfo,
 		resultRelInfo->ri_NumGeneratedNeededI = ri_NumGeneratedNeeded;
 	}
 
-	resultRelInfo->ri_Generated_valid = true;
-
 	MemoryContextSwitchTo(oldContext);
 }
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index fe6070381f6..62166561d37 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -490,6 +490,8 @@ typedef struct ResultRelInfo
 
 	/* For UPDATE, attnums of generated columns to be computed */
 	Bitmapset  *ri_extraUpdatedCols;
+	/* true if the above has been computed */
+	bool		ri_extraUpdatedCols_valid;
 
 	/* Projection to generate new tuple in an INSERT/UPDATE */
 	ProjectionInfo *ri_projectNew;
@@ -556,9 +558,6 @@ typedef struct ResultRelInfo
 	int			ri_NumGeneratedNeededI;
 	int			ri_NumGeneratedNeededU;
 
-	/* true if the above have been computed */
-	bool		ri_Generated_valid;
-
 	/* list of RETURNING expressions */
 	List	   *ri_returningList;
 
-- 
2.48.1

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 4 Feb 2025 23:36:53 +0100

> an ERROR as the unpublished generated column 'b' is part of the

Thanks, I have fixed that in v14.  (The other 4 tests were correct, right?)

---

From: Dean Rasheed <[email-masked]>
Date: Wed, 5 Feb 2025 23:25:30 +0000

> Yeah, this is quite contorted.  I have renamed it like you suggested.

I looked over this again and I think the patch is in good shape to be committed.

One thought that occurred to me was whether it would be better for the
psql describe output (and maybe also pg_dump) to explicitly output
"virtual" for columns of this kind. I know that that's the default for
generated columns, but someone reading the output might not know or
remember that, so perhaps it would be helpful to be explicit.

Regards,
Dean

---

From: vignesh C <[email-masked]>
Date: Thu, 6 Feb 2025 18:33:11 +0530

> Yeah, this is quite contorted.  I have renamed it like you suggested.

One suggestion: for the option where the user specifies
publish_generated_columns as virtual (as shown below), could we change
the error indicating that virtual generated columns are not currently
supported?
CREATE PUBLICATION pub1 FOR TABLE t1 WITH (publish_generated_columns = virtual);

Also, could we add a XXX comment in either decode.c, pgoutput.c, or
publicationcmds.c outlining what would be needed to support the
replication of virtual generated columns? Specifically, it would be
helpful if we could include how to retrieve virtual generated column
data during decoding. This would serve as a reference for anyone
working on enabling logical replication of virtual generated columns
in the future.

Regards,
Vignesh

---

From: Peter Eisentraut <[email-masked]>
Date: Fri, 7 Feb 2025 13:34:47 +0100

> I looked over this again and I think the patch is in good shape to be committed.

I've committed it.  Thanks.

> remember that, so perhaps it would be helpful to be explicit.

My preference was to have the default output format use only 
SQL-standard syntax, not extensions like "VIRTUAL".  If people hate 
this, we can easily change it.

---

From: Peter Eisentraut <[email-masked]>
Date: Fri, 7 Feb 2025 13:36:28 +0100

> working on enabling logical replication of virtual generated columns

I think adding support for virtual generated columns in logical 
replication would require a lot more work than filling in the handful of 
places that we know about.  (Otherwise, we'd already have done it now.) 
So I'd rather not give potentially misleading or incomplete advice.

---

From: Alexander Lakhin <[email-masked]>
Date: Sun, 9 Feb 2025 10:00:00 +0200

Hello Peter,

> I've committed it.  Thanks.

Please look at a planner error with a virtual generated column triggered
by the following script:
CREATE TABLE t(a int, b int GENERATED ALWAYS AS (a * 1));

SELECT SUM(CASE WHEN t.b = 1 THEN 1 ELSE 1 END) OVER (PARTITION BY t.a)
FROM t AS t1 LEFT JOIN T ON true;

ERROR:  XX000: wrong varnullingrels (b) (expected (b 3)) for Var 2/1
LOCATION:  search_indexed_tlist_for_var, setrefs.c:2901

(I discovered this anomaly with SQLsmith.)

Best regards,
Alexander Lakhin
Neon (https://neon.tech)