]> granicus.if.org Git - postgresql/commitdiff
Generated columns
authorPeter Eisentraut <peter@eisentraut.org>
Sat, 30 Mar 2019 07:13:09 +0000 (08:13 +0100)
committerPeter Eisentraut <peter@eisentraut.org>
Sat, 30 Mar 2019 07:15:57 +0000 (08:15 +0100)
This is an SQL-standard feature that allows creating columns that are
computed from expressions rather than assigned, similar to a view or
materialized view but on a column basis.

This implements one kind of generated column: stored (computed on
write).  Another kind, virtual (computed on read), is planned for the
future, and some room is left for it.

Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Pavel Stehule <pavel.stehule@gmail.com>
Discussion: https://www.postgresql.org/message-id/flat/b151f851-4019-bdb1-699e-ebab07d2f40a@2ndquadrant.com

84 files changed:
contrib/postgres_fdw/expected/postgres_fdw.out
contrib/postgres_fdw/postgres_fdw.c
contrib/postgres_fdw/sql/postgres_fdw.sql
doc/src/sgml/catalogs.sgml
doc/src/sgml/ddl.sgml
doc/src/sgml/information_schema.sgml
doc/src/sgml/protocol.sgml
doc/src/sgml/ref/copy.sgml
doc/src/sgml/ref/create_foreign_table.sgml
doc/src/sgml/ref/create_table.sgml
doc/src/sgml/ref/create_trigger.sgml
doc/src/sgml/textsearch.sgml
doc/src/sgml/trigger.sgml
src/backend/access/common/tupdesc.c
src/backend/catalog/heap.c
src/backend/catalog/information_schema.sql
src/backend/commands/copy.c
src/backend/commands/tablecmds.c
src/backend/commands/trigger.c
src/backend/commands/typecmds.c
src/backend/executor/execMain.c
src/backend/executor/execReplication.c
src/backend/executor/nodeModifyTable.c
src/backend/nodes/copyfuncs.c
src/backend/nodes/equalfuncs.c
src/backend/nodes/outfuncs.c
src/backend/nodes/readfuncs.c
src/backend/optimizer/plan/createplan.c
src/backend/optimizer/util/inherit.c
src/backend/optimizer/util/plancat.c
src/backend/parser/analyze.c
src/backend/parser/gram.y
src/backend/parser/parse_agg.c
src/backend/parser/parse_expr.c
src/backend/parser/parse_func.c
src/backend/parser/parse_relation.c
src/backend/parser/parse_utilcmd.c
src/backend/replication/logical/proto.c
src/backend/replication/logical/relation.c
src/backend/replication/logical/tablesync.c
src/backend/replication/logical/worker.c
src/backend/replication/pgoutput/pgoutput.c
src/backend/rewrite/rewriteHandler.c
src/backend/utils/cache/lsyscache.c
src/backend/utils/cache/partcache.c
src/backend/utils/cache/relcache.c
src/bin/pg_dump/pg_dump.c
src/bin/pg_dump/pg_dump.h
src/bin/pg_dump/pg_dump_sort.c
src/bin/pg_dump/t/002_pg_dump.pl
src/bin/psql/describe.c
src/include/access/tupdesc.h
src/include/catalog/catversion.h
src/include/catalog/heap.h
src/include/catalog/pg_attribute.h
src/include/catalog/pg_class.dat
src/include/executor/nodeModifyTable.h
src/include/nodes/execnodes.h
src/include/nodes/parsenodes.h
src/include/optimizer/plancat.h
src/include/parser/kwlist.h
src/include/parser/parse_node.h
src/include/utils/lsyscache.h
src/pl/plperl/expected/plperl_trigger.out
src/pl/plperl/plperl.c
src/pl/plperl/sql/plperl_trigger.sql
src/pl/plpgsql/src/pl_exec.c
src/pl/plpython/expected/plpython_trigger.out
src/pl/plpython/plpy_cursorobject.c
src/pl/plpython/plpy_exec.c
src/pl/plpython/plpy_spi.c
src/pl/plpython/plpy_typeio.c
src/pl/plpython/plpy_typeio.h
src/pl/plpython/sql/plpython_trigger.sql
src/pl/tcl/expected/pltcl_trigger.out
src/pl/tcl/pltcl.c
src/pl/tcl/sql/pltcl_trigger.sql
src/test/regress/expected/create_table_like.out
src/test/regress/expected/generated.out [new file with mode: 0644]
src/test/regress/parallel_schedule
src/test/regress/serial_schedule
src/test/regress/sql/create_table_like.sql
src/test/regress/sql/generated.sql [new file with mode: 0644]
src/test/subscription/t/011_generated.pl [new file with mode: 0644]

index 9ad9035c5df1823cfbedd7961701832e8ae34ad2..bab9def927e276aa741392976981b2ba4d0581b0 100644 (file)
@@ -6431,6 +6431,31 @@ select * from rem1;
  11 | bye remote
 (4 rows)
 
+-- ===================================================================
+-- test generated columns
+-- ===================================================================
+create table gloc1 (a int, b 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');
+insert into grem1 (a) values (1), (2);
+update grem1 set a = 22 where a = 2;
+select * from gloc1;
+ a  | b  
+----+----
+  1 |  2
+ 22 | 44
+(2 rows)
+
+select * from grem1;
+ a  | b  
+----+----
+  1 |  2
+ 22 | 44
+(2 rows)
+
 -- ===================================================================
 -- test local triggers
 -- ===================================================================
index 2f387fac422a11dd4fb0e011223cbe199d947a9f..d0d36aaa0dcecc7630b15a2ba6a994a8f225ad6e 100644 (file)
@@ -1644,9 +1644,10 @@ postgresPlanForeignModify(PlannerInfo *root,
        else if (operation == CMD_UPDATE)
        {
                int                     col;
+               Bitmapset  *allUpdatedCols = bms_union(rte->updatedCols, rte->extraUpdatedCols);
 
                col = -1;
-               while ((col = bms_next_member(rte->updatedCols, col)) >= 0)
+               while ((col = bms_next_member(allUpdatedCols, col)) >= 0)
                {
                        /* bit numbers are offset by FirstLowInvalidHeapAttributeNumber */
                        AttrNumber      attno = col + FirstLowInvalidHeapAttributeNumber;
index 4728511abf3b5f836f81b700febfa02433be96ba..a96c327134800733106f8a21d6721aaec92cbd2a 100644 (file)
@@ -1363,6 +1363,20 @@ insert into rem1(f2) values('bye remote');
 select * from loc1;
 select * from rem1;
 
+-- ===================================================================
+-- test generated columns
+-- ===================================================================
+create table gloc1 (a int, b 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');
+insert into grem1 (a) values (1), (2);
+update grem1 set a = 22 where a = 2;
+select * from gloc1;
+select * from grem1;
+
 -- ===================================================================
 -- test local triggers
 -- ===================================================================
index 026c422cd223658f98a1cc0e04e77ba59de31d6e..f4aabf5dc7f488b9b4dade2b7af77e208cfc2daf 100644 (file)
       <entry><type>bool</type></entry>
       <entry></entry>
       <entry>
-       This column has a default value, in which case there will be a
-       corresponding entry in the <structname>pg_attrdef</structname>
-       catalog that actually defines the value.
+       This column has a default expression or generation expression, in which
+       case there will be a corresponding entry in the
+       <structname>pg_attrdef</structname> catalog that actually defines the
+       expression.  (Check <structfield>attgenerated</structfield> to
+       determine whether this is a default or a generation expression.)
       </entry>
      </row>
 
       </entry>
      </row>
 
+     <row>
+      <entry><structfield>attgenerated</structfield></entry>
+      <entry><type>char</type></entry>
+      <entry></entry>
+      <entry>
+       If a zero byte (<literal>''</literal>), then not a generated column.
+       Otherwise, <literal>s</literal> = stored.  (Other values might be added
+       in the future.)
+      </entry>
+     </row>
+
      <row>
       <entry><structfield>attisdropped</structfield></entry>
       <entry><type>bool</type></entry>
index 110f6b46570e03ff1b090e40cb15306422b191ca..1fe27c5da9f5c371e585750a4867279fad35bc1c 100644 (file)
@@ -233,6 +233,124 @@ CREATE TABLE products (
   </para>
  </sect1>
 
+ <sect1 id="ddl-generated-columns">
+  <title>Generated Columns</title>
+
+  <indexterm zone="ddl-generated-columns">
+   <primary>generated column</primary>
+  </indexterm>
+
+  <para>
+   A generated column is a special column that is always computed from other
+   columns.  Thus, it is for columns what a view is for tables.  There are two
+   kinds of generated columns: stored and virtual.  A stored generated column
+   is computed when it is written (inserted or updated) and occupies storage
+   as if it were a normal column.  A virtual generated column occupies no
+   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).
+   PostgreSQL currently implements only stored generated columns.
+  </para>
+
+  <para>
+   To create a generated column, use the <literal>GENERATED ALWAYS
+   AS</literal> clause in <command>CREATE TABLE</command>, for example:
+<programlisting>
+CREATE TABLE people (
+    ...,
+    height_cm numeric,
+    height_in numeric <emphasis>GENERATED ALWAYS AS (height_cm * 2.54) STORED</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.
+  </para>
+
+  <para>
+   A generated column cannot be written to directly.  In
+   <command>INSERT</command> or <command>UPDATE</command> commands, a value
+   cannot be specified for a generated column, but the keyword
+   <literal>DEFAULT</literal> may be specified.
+  </para>
+
+  <para>
+   Consider the differences between a column with a default and a generated
+   column.  The column default is evaluated once when the row is first
+   inserted if no other value was provided; a generated column is updated
+   whenever the row changes and cannot be overridden.  A column default may
+   not refer to other columns of the table; a generation expression would
+   normally do so.  A column default can use volatile functions, for example
+   <literal>random()</literal> or functions referring to the current time;
+   this is not allowed for generated columns.
+  </para>
+
+  <para>
+   Several restrictions apply to the definition of generated columns and
+   tables involving generated columns:
+
+   <itemizedlist>
+    <listitem>
+     <para>
+      The generation expression can only use immutable functions and cannot
+      use subqueries or reference anything other than the current row in any
+      way.
+     </para>
+    </listitem>
+    <listitem>
+     <para>
+      A generation expression cannot reference another generated column.
+     </para>
+    </listitem>
+    <listitem>
+     <para>
+      A generation expression cannot reference a system column, except
+      <varname>tableoid</varname>.
+     </para>
+    </listitem>
+    <listitem>
+     <para>
+      A generated column cannot have a column default or an identity definition.
+     </para>
+    </listitem>
+    <listitem>
+     <para>
+      A generated column cannot be part of a partition key.
+     </para>
+    </listitem>
+    <listitem>
+     <para>
+      Foreign tables can have generated columns.  See <xref
+      linkend="sql-createforeigntable"/> for details.
+     </para>
+    </listitem>
+   </itemizedlist>
+  </para>
+
+  <para>
+   Additional considerations apply to the use of generated columns.
+   <itemizedlist>
+    <listitem>
+     <para>
+      Generated columns maintain access privileges separately from their
+      underlying base columns.  So, it is possible to arrange it so that a
+      particular role can read from a generated column but not from the
+      underlying base columns.
+     </para>
+    </listitem>
+    <listitem>
+     <para>
+      Generated columns are, conceptually, updated after
+      <literal>BEFORE</literal> triggers have run.  Therefore, changes made to
+      base columns in a <literal>BEFORE</literal> trigger will be reflected in
+      generated columns.  But conversely, it is not allowed to access
+      generated columns in <literal>BEFORE</literal> triggers.
+     </para>
+    </listitem>
+   </itemizedlist>
+  </para>
+ </sect1>
+
  <sect1 id="ddl-constraints">
   <title>Constraints</title>
 
index b13700da924155b76192ee3f192a3a4267dd9a2c..1321ade44a3a6c6603864f80410b57c2bb52d79e 100644 (file)
   </table>
  </sect1>
 
+ <sect1 id="infoschema-column-column-usage">
+  <title><literal>column_column_usage</literal></title>
+
+  <para>
+   The view <literal>column_column_usage</literal> identifies all generated
+   columns that depend on another base column in the same table.  Only tables
+   owned by a currently enabled role are included.
+  </para>
+
+  <table>
+   <title><literal>column_column_usage</literal> Columns</title>
+
+   <tgroup cols="3">
+    <thead>
+     <row>
+      <entry>Name</entry>
+      <entry>Data Type</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry><literal>table_catalog</literal></entry>
+      <entry><type>sql_identifier</type></entry>
+      <entry>Name of the database containing the table (always the current database)</entry>
+     </row>
+
+     <row>
+      <entry><literal>table_schema</literal></entry>
+      <entry><type>sql_identifier</type></entry>
+      <entry>Name of the schema containing the table</entry>
+     </row>
+
+     <row>
+      <entry><literal>table_name</literal></entry>
+      <entry><type>sql_identifier</type></entry>
+      <entry>Name of the table</entry>
+     </row>
+
+     <row>
+      <entry><literal>column_name</literal></entry>
+      <entry><type>sql_identifier</type></entry>
+      <entry>Name of the base column that a generated column depends on</entry>
+     </row>
+
+     <row>
+      <entry><literal>dependent_column</literal></entry>
+      <entry><type>sql_identifier</type></entry>
+      <entry>Name of the generated column</entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="infoschema-column-domain-usage">
   <title><literal>column_domain_usage</literal></title>
 
      <row>
       <entry><literal>is_generated</literal></entry>
       <entry><type>character_data</type></entry>
-      <entry>Applies to a feature not available in <productname>PostgreSQL</productname></entry>
+      <entry>
+       If the column is a generated column, then <literal>ALWAYS</literal>,
+       else <literal>NEVER</literal>.
+      </entry>
      </row>
 
      <row>
       <entry><literal>generation_expression</literal></entry>
       <entry><type>character_data</type></entry>
-      <entry>Applies to a feature not available in <productname>PostgreSQL</productname></entry>
+      <entry>
+       If the column is a generated column, then the generation expression,
+       else null.
+      </entry>
      </row>
 
      <row>
index d66b860cbd7d588a5add2d60a56d4af533b86825..a0e1f78bfc81981c4a231c2cbf5bdd7d2220d486 100644 (file)
@@ -6450,7 +6450,7 @@ Relation
 </listitem>
 </varlistentry>
 </variablelist>
-        Next, the following message part appears for each column:
+        Next, the following message part appears for each column (except generated columns):
 <variablelist>
 <varlistentry>
 <term>
@@ -6875,7 +6875,7 @@ TupleData
 </listitem>
 </varlistentry>
 </variablelist>
-        Next, one of the following submessages appears for each column:
+        Next, one of the following submessages appears for each column (except generated columns):
 <variablelist>
 <varlistentry>
 <term>
index 254d3ab8eb3a8607337c369722f31aaa921658a0..5e2992ddacc5f42a9d1fd98011fd6acaaa0416cc 100644 (file)
@@ -103,7 +103,8 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
      <listitem>
      <para>
       An optional list of columns to be copied.  If no column list is
-      specified, all columns of the table will be copied.
+      specified, all columns of the table except generated columns will be
+      copied.
      </para>
     </listitem>
    </varlistentry>
index 19eb5341e7da94b1c0bb10d8ee7e189fe1cb6fd6..65ba3e3d373a0b115d4752aeca8036eec9c24ed1 100644 (file)
@@ -42,7 +42,8 @@ CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name
 { NOT NULL |
   NULL |
   CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
-  DEFAULT <replaceable>default_expr</replaceable> }
+  DEFAULT <replaceable>default_expr</replaceable> |
+  GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED }
 
 <phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
 
@@ -258,6 +259,30 @@ CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ]
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED</literal><indexterm><primary>generated column</primary></indexterm></term>
+    <listitem>
+     <para>
+      This clause creates the column as a <firstterm>generated
+      column</firstterm>.  The column cannot be written to, and when read it
+      will be computed from the specified expression.
+     </para>
+
+     <para>
+      The keyword <literal>STORED</literal> is required to signify that the
+      column will be computed on write.  (The computed value will be presented
+      to the foreign-data wrapper for storage and must be returned on
+      reading.)
+     </para>
+
+     <para>
+      The generation expression can refer to other columns in the table, but
+      not other generated columns.  Any functions and operators used must be
+      immutable.  References to other tables are not allowed.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><replaceable class="parameter">server_name</replaceable></term>
     <listitem>
index 166078410c2824c9df75d661d499e96c6bab9795..0172fa3b4bec2f83e96b1bab2a92dccdab061233 100644 (file)
@@ -62,6 +62,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
   NULL |
   CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
   DEFAULT <replaceable>default_expr</replaceable> |
+  GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED |
   GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( <replaceable>sequence_options</replaceable> ) ] |
   UNIQUE <replaceable class="parameter">index_parameters</replaceable> |
   PRIMARY KEY <replaceable class="parameter">index_parameters</replaceable> |
@@ -83,7 +84,7 @@ class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable cl
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | CONSTRAINTS | DEFAULTS | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -627,6 +628,16 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
         </listitem>
        </varlistentry>
 
+       <varlistentry>
+        <term><literal>INCLUDING GENERATED</literal></term>
+        <listitem>
+         <para>
+          Any generation expressions of copied column definitions will be
+          copied.  By default, new columns will be regular base columns.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry>
         <term><literal>INCLUDING IDENTITY</literal></term>
         <listitem>
@@ -797,6 +808,28 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>GENERATED ALWAYS AS ( <replaceable>generation_expr</replaceable> ) STORED</literal><indexterm><primary>generated column</primary></indexterm></term>
+    <listitem>
+     <para>
+      This clause creates the column as a <firstterm>generated
+      column</firstterm>.  The column cannot be written to, and when read it
+      will be computed from the specified expression.
+     </para>
+
+     <para>
+      The keyword <literal>STORED</literal> is required to signify that the
+      column will be computed on write and will be stored on disk.  default.
+     </para>
+
+     <para>
+      The generation expression can refer to other columns in the table, but
+      not other generated columns.  Any functions and operators used must be
+      immutable.  References to other tables are not allowed.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( <replaceable>sequence_options</replaceable> ) ]</literal></term>
     <listitem>
@@ -2028,6 +2061,16 @@ CREATE TABLE cities_partdef
    </para>
   </refsect2>
 
+  <refsect2>
+   <title>Generated Columns</title>
+
+   <para>
+    The option <literal>STORED</literal> is not standard but is also used by
+    other SQL implementations.  The SQL standard does not specify the storage
+    of generated columns.
+   </para>
+  </refsect2>
+
   <refsect2>
    <title><literal>LIKE</literal> Clause</title>
 
index 6514ffc6ae63f72a5c5cbb9f76c3e62f6922389b..6456105de61d826d680cc869f1f2a9b849ec5510 100644 (file)
@@ -261,7 +261,9 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="parameter">name</replaceable>
 UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</replaceable> ... ]
 </synopsis>
       The trigger will only fire if at least one of the listed columns
-      is mentioned as a target of the <command>UPDATE</command> command.
+      is mentioned as a target of the <command>UPDATE</command> command
+      or if one of the listed columns is a generated column that depends on a
+      column that is the target of the <command>UPDATE</command>.
      </para>
 
      <para>
index 3281f7cd33af64547d5cb49aedafa5af49ec9b9d..40888a4d207500b4e28d7851df36d58cc0cac25f 100644 (file)
@@ -620,15 +620,17 @@ CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' |
 
    <para>
     Another approach is to create a separate <type>tsvector</type> column
-    to hold the output of <function>to_tsvector</function>.  This example is a
+    to hold the output of <function>to_tsvector</function>.  To keep this
+    column automatically up to date with its source data, use a stored
+    generated column.  This example is a
     concatenation of <literal>title</literal> and <literal>body</literal>,
     using <function>coalesce</function> to ensure that one field will still be
     indexed when the other is <literal>NULL</literal>:
 
 <programlisting>
-ALTER TABLE pgweb ADD COLUMN textsearchable_index_col tsvector;
-UPDATE pgweb SET textsearchable_index_col =
-     to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,''));
+ALTER TABLE pgweb
+    ADD COLUMN textsearchable_index_col tsvector
+               GENERATED ALWAYS AS (to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))) STORED;
 </programlisting>
 
     Then we create a <acronym>GIN</acronym> index to speed up the search:
@@ -648,14 +650,6 @@ LIMIT 10;
 </programlisting>
    </para>
 
-   <para>
-    When using a separate column to store the <type>tsvector</type>
-    representation,
-    it is necessary to create a trigger to keep the <type>tsvector</type>
-    column current anytime <literal>title</literal> or <literal>body</literal> changes.
-    <xref linkend="textsearch-update-triggers"/> explains how to do that.
-   </para>
-
    <para>
     One advantage of the separate-column approach over an expression index
     is that it is not necessary to explicitly specify the text search
@@ -1857,6 +1851,14 @@ SELECT ts_rewrite('a &amp; b'::tsquery,
     <secondary>for updating a derived tsvector column</secondary>
    </indexterm>
 
+   <note>
+    <para>
+     The method described in this section has been obsoleted by the use of
+     stored generated columns, as described in <xref
+     linkend="textsearch-tables-index"/>.
+    </para>
+   </note>
+
    <para>
     When using a separate column to store the <type>tsvector</type> representation
     of your documents, it is necessary to create a trigger to update the
index be9c2284481b0b7a075c30c0333b6efb7b822aef..67e1861e0619e97c97eda0905fdd8139abc500ed 100644 (file)
     operation, and so they can return <symbol>NULL</symbol>.
    </para>
 
+   <para>
+    Some considerations apply for generated
+    columns.<indexterm><primary>generated column</primary><secondary>in
+    triggers</secondary></indexterm>  Stored generated columns are computed after
+    <literal>BEFORE</literal> triggers and before <literal>AFTER</literal>
+    triggers.  Therefore, the generated value can be inspected in
+    <literal>AFTER</literal> triggers.  In <literal>BEFORE</literal> triggers,
+    the <literal>OLD</literal> row contains the old generated value, as one
+    would expect, but the <literal>NEW</literal> row does not yet contain the
+    new generated value and should not be accessed.  In the C language
+    interface, the content of the column is undefined at this point; a
+    higher-level programming language should prevent access to a stored
+    generated column in the <literal>NEW</literal> row in a
+    <literal>BEFORE</literal> trigger.  Changes to the value of a generated
+    column in a <literal>BEFORE</literal> trigger are ignored and will be
+    overwritten.
+   </para>
+
    <para>
     If more than one trigger is defined for the same event on the same
     relation, the triggers will be fired in alphabetical order by
index 0158950a432a6d8e8d9204a2d421a2b19bdd0fb9..6bc4e4c036097e33c2148cfca08f4f13fffe0943 100644 (file)
@@ -131,6 +131,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
                att->atthasdef = false;
                att->atthasmissing = false;
                att->attidentity = '\0';
+               att->attgenerated = '\0';
        }
 
        /* We can copy the tuple type identification, too */
@@ -165,6 +166,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
                TupleConstr *cpy = (TupleConstr *) palloc0(sizeof(TupleConstr));
 
                cpy->has_not_null = constr->has_not_null;
+               cpy->has_generated_stored = constr->has_generated_stored;
 
                if ((cpy->num_defval = constr->num_defval) > 0)
                {
@@ -247,6 +249,7 @@ TupleDescCopy(TupleDesc dst, TupleDesc src)
                att->atthasdef = false;
                att->atthasmissing = false;
                att->attidentity = '\0';
+               att->attgenerated = '\0';
        }
        dst->constr = NULL;
 
@@ -300,6 +303,7 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
        dstAtt->atthasdef = false;
        dstAtt->atthasmissing = false;
        dstAtt->attidentity = '\0';
+       dstAtt->attgenerated = '\0';
 }
 
 /*
@@ -456,6 +460,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
                        return false;
                if (attr1->attidentity != attr2->attidentity)
                        return false;
+               if (attr1->attgenerated != attr2->attgenerated)
+                       return false;
                if (attr1->attisdropped != attr2->attisdropped)
                        return false;
                if (attr1->attislocal != attr2->attislocal)
@@ -476,6 +482,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
                        return false;
                if (constr1->has_not_null != constr2->has_not_null)
                        return false;
+               if (constr1->has_generated_stored != constr2->has_generated_stored)
+                       return false;
                n = constr1->num_defval;
                if (n != (int) constr2->num_defval)
                        return false;
@@ -638,6 +646,7 @@ TupleDescInitEntry(TupleDesc desc,
        att->atthasdef = false;
        att->atthasmissing = false;
        att->attidentity = '\0';
+       att->attgenerated = '\0';
        att->attisdropped = false;
        att->attislocal = true;
        att->attinhcount = 0;
@@ -697,6 +706,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
        att->atthasdef = false;
        att->atthasmissing = false;
        att->attidentity = '\0';
+       att->attgenerated = '\0';
        att->attisdropped = false;
        att->attislocal = true;
        att->attinhcount = 0;
@@ -853,6 +863,7 @@ BuildDescForRelation(List *schema)
                TupleConstr *constr = (TupleConstr *) palloc0(sizeof(TupleConstr));
 
                constr->has_not_null = true;
+               constr->has_generated_stored = false;
                constr->defval = NULL;
                constr->missing = NULL;
                constr->num_defval = 0;
index 71ad8c43c32d757d01b49f34395d21355235995c..6b77eff0af1969af244d708132d1bb290ce49802 100644 (file)
@@ -71,6 +71,7 @@
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
 #include "parser/parse_relation.h"
+#include "parser/parsetree.h"
 #include "partitioning/partdesc.h"
 #include "storage/lmgr.h"
 #include "storage/predicate.h"
@@ -718,6 +719,7 @@ InsertPgAttributeTuple(Relation pg_attribute_rel,
        values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(new_attribute->atthasdef);
        values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(new_attribute->atthasmissing);
        values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(new_attribute->attidentity);
+       values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(new_attribute->attgenerated);
        values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(new_attribute->attisdropped);
        values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(new_attribute->attislocal);
        values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(new_attribute->attinhcount);
@@ -1631,6 +1633,9 @@ RemoveAttributeById(Oid relid, AttrNumber attnum)
                /* We don't want to keep stats for it anymore */
                attStruct->attstattarget = 0;
 
+               /* Unset this so no one tries to look up the generation expression */
+               attStruct->attgenerated = '\0';
+
                /*
                 * Change the column name to something that isn't likely to conflict
                 */
@@ -2130,6 +2135,7 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
        Relation        attrrel;
        HeapTuple       atttup;
        Form_pg_attribute attStruct;
+       char            attgenerated;
        Oid                     attrdefOid;
        ObjectAddress colobject,
                                defobject;
@@ -2177,6 +2183,7 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
                elog(ERROR, "cache lookup failed for attribute %d of relation %u",
                         attnum, RelationGetRelid(rel));
        attStruct = (Form_pg_attribute) GETSTRUCT(atttup);
+       attgenerated = attStruct->attgenerated;
        if (!attStruct->atthasdef)
        {
                Form_pg_attribute defAttStruct;
@@ -2197,7 +2204,7 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
                valuesAtt[Anum_pg_attribute_atthasdef - 1] = true;
                replacesAtt[Anum_pg_attribute_atthasdef - 1] = true;
 
-               if (add_column_mode)
+               if (add_column_mode && !attgenerated)
                {
                        expr2 = expression_planner(expr2);
                        estate = CreateExecutorState();
@@ -2259,7 +2266,26 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
        /*
         * Record dependencies on objects used in the expression, too.
         */
-       recordDependencyOnExpr(&defobject, expr, NIL, DEPENDENCY_NORMAL);
+       if (attgenerated)
+       {
+               /*
+                * Generated column: Dropping anything that the generation expression
+                * refers to automatically drops the generated column.
+                */
+               recordDependencyOnSingleRelExpr(&colobject, expr, RelationGetRelid(rel),
+                                                                               DEPENDENCY_AUTO,
+                                                                               DEPENDENCY_AUTO, false);
+       }
+       else
+       {
+               /*
+                * Normal default: Dropping anything that the default refers to
+                * requires CASCADE and drops the default only.
+                */
+               recordDependencyOnSingleRelExpr(&defobject, expr, RelationGetRelid(rel),
+                                                                               DEPENDENCY_NORMAL,
+                                                                               DEPENDENCY_NORMAL, false);
+       }
 
        /*
         * Post creation hook for attribute defaults.
@@ -2517,12 +2543,14 @@ AddRelationNewConstraints(Relation rel,
 
                expr = cookDefault(pstate, colDef->raw_default,
                                                   atp->atttypid, atp->atttypmod,
-                                                  NameStr(atp->attname));
+                                                  NameStr(atp->attname),
+                                                  atp->attgenerated);
 
                /*
                 * If the expression is just a NULL constant, we do not bother to make
                 * an explicit pg_attrdef entry, since the default behavior is
-                * equivalent.
+                * equivalent.  This applies to column defaults, but not for generation
+                * expressions.
                 *
                 * Note a nonobvious property of this test: if the column is of a
                 * domain type, what we'll get is not a bare null Const but a
@@ -2531,7 +2559,9 @@ AddRelationNewConstraints(Relation rel,
                 * override any default that the domain might have.
                 */
                if (expr == NULL ||
-                       (IsA(expr, Const) &&((Const *) expr)->constisnull))
+                       (!colDef->generated &&
+                        IsA(expr, Const) &&
+                        castNode(Const, expr)->constisnull))
                        continue;
 
                /* If the DEFAULT is volatile we cannot use a missing value */
@@ -2888,6 +2918,46 @@ SetRelationNumChecks(Relation rel, int numchecks)
        table_close(relrel, RowExclusiveLock);
 }
 
+/*
+ * Check for references to generated columns
+ */
+static bool
+check_nested_generated_walker(Node *node, void *context)
+{
+       ParseState *pstate = context;
+
+       if (node == NULL)
+               return false;
+       else if (IsA(node, Var))
+       {
+               Var                *var = (Var *) node;
+               Oid                     relid;
+               AttrNumber      attnum;
+
+               relid = rt_fetch(var->varno, pstate->p_rtable)->relid;
+               attnum = var->varattno;
+
+               if (OidIsValid(relid) && AttributeNumberIsValid(attnum) && get_attgenerated(relid, attnum))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_SYNTAX_ERROR),
+                                        errmsg("cannot use generated column \"%s\" in column generation expression",
+                                                       get_attname(relid, attnum, false)),
+                                        errdetail("A generated column cannot reference another generated column."),
+                                        parser_errposition(pstate, var->location)));
+
+               return false;
+       }
+       else
+               return expression_tree_walker(node, check_nested_generated_walker,
+                                                                         (void *) context);
+}
+
+static void
+check_nested_generated(ParseState *pstate, Node *node)
+{
+       check_nested_generated_walker(node, pstate);
+}
+
 /*
  * Take a raw default and convert it to a cooked format ready for
  * storage.
@@ -2905,7 +2975,8 @@ cookDefault(ParseState *pstate,
                        Node *raw_default,
                        Oid atttypid,
                        int32 atttypmod,
-                       const char *attname)
+                       const char *attname,
+                       char attgenerated)
 {
        Node       *expr;
 
@@ -2914,14 +2985,25 @@ cookDefault(ParseState *pstate,
        /*
         * Transform raw parsetree to executable expression.
         */
-       expr = transformExpr(pstate, raw_default, EXPR_KIND_COLUMN_DEFAULT);
+       expr = transformExpr(pstate, raw_default, attgenerated ? EXPR_KIND_GENERATED_COLUMN : EXPR_KIND_COLUMN_DEFAULT);
 
-       /*
-        * transformExpr() should have already rejected column references,
-        * subqueries, aggregates, window functions, and SRFs, based on the
-        * EXPR_KIND_ for a default expression.
-        */
-       Assert(!contain_var_clause(expr));
+       if (attgenerated)
+       {
+               check_nested_generated(pstate, expr);
+
+               if (contain_mutable_functions(expr))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                        errmsg("generation expression is not immutable")));
+       }
+       else
+       {
+               /*
+                * For a default expression, transformExpr() should have rejected
+                * column references.
+                */
+               Assert(!contain_var_clause(expr));
+       }
 
        /*
         * Coerce the expression to the correct type and typmod, if given. This
index 94e482596f8fcb5187b5fbd53d49894018343338..16677e78d62575d666a9a43b5a1bddecffb98378 100644 (file)
@@ -509,7 +509,29 @@ GRANT SELECT ON collation_character_set_applicability TO PUBLIC;
  * COLUMN_COLUMN_USAGE view
  */
 
--- feature not supported
+CREATE VIEW column_column_usage AS
+    SELECT CAST(current_database() AS sql_identifier) AS table_catalog,
+           CAST(n.nspname AS sql_identifier) AS table_schema,
+           CAST(c.relname AS sql_identifier) AS table_name,
+           CAST(ac.attname AS sql_identifier) AS column_name,
+           CAST(ad.attname AS sql_identifier) AS dependent_column
+
+    FROM pg_namespace n, pg_class c, pg_depend d,
+         pg_attribute ac, pg_attribute ad
+
+    WHERE n.oid = c.relnamespace
+          AND c.oid = ac.attrelid
+          AND c.oid = ad.attrelid
+          AND d.classid = 'pg_catalog.pg_class'::regclass
+          AND d.refclassid = 'pg_catalog.pg_class'::regclass
+          AND d.objid = d.refobjid
+          AND c.oid = d.objid
+          AND d.objsubid = ad.attnum
+          AND d.refobjsubid = ac.attnum
+          AND ad.attgenerated <> ''
+          AND pg_has_role(c.relowner, 'USAGE');
+
+GRANT SELECT ON column_column_usage TO PUBLIC;
 
 
 /*
@@ -656,7 +678,7 @@ CREATE VIEW columns AS
            CAST(c.relname AS sql_identifier) AS table_name,
            CAST(a.attname AS sql_identifier) AS column_name,
            CAST(a.attnum AS cardinal_number) AS ordinal_position,
-           CAST(pg_get_expr(ad.adbin, ad.adrelid) AS character_data) AS column_default,
+           CAST(CASE WHEN a.attgenerated = '' THEN pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS column_default,
            CAST(CASE WHEN a.attnotnull OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END
              AS yes_or_no)
              AS is_nullable,
@@ -745,8 +767,8 @@ CREATE VIEW columns AS
            CAST(seq.seqmin AS character_data) AS identity_minimum,
            CAST(CASE WHEN seq.seqcycle THEN 'YES' ELSE 'NO' END AS yes_or_no) AS identity_cycle,
 
-           CAST('NEVER' AS character_data) AS is_generated,
-           CAST(null AS character_data) AS generation_expression,
+           CAST(CASE WHEN a.attgenerated <> '' THEN 'ALWAYS' ELSE 'NEVER' END AS character_data) AS is_generated,
+           CAST(CASE WHEN a.attgenerated <> '' THEN pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS generation_expression,
 
            CAST(CASE WHEN c.relkind IN ('r', 'p') OR
                           (c.relkind IN ('v', 'f') AND
index 705df8900ba25901aeb7e7f5ee898e63a31e6e14..27d3a012afa0611651f7b96d16bdef2670ac7c08 100644 (file)
@@ -32,6 +32,7 @@
 #include "commands/trigger.h"
 #include "executor/execPartition.h"
 #include "executor/executor.h"
+#include "executor/nodeModifyTable.h"
 #include "executor/tuptable.h"
 #include "foreign/fdwapi.h"
 #include "libpq/libpq.h"
@@ -2922,6 +2923,21 @@ CopyFrom(CopyState cstate)
                        }
                        else
                        {
+                               /*
+                                * Compute stored generated columns
+                                *
+                                * Switch memory context so that the new tuple is in the same
+                                * context as the old one.
+                                */
+                               if (resultRelInfo->ri_RelationDesc->rd_att->constr &&
+                                       resultRelInfo->ri_RelationDesc->rd_att->constr->has_generated_stored)
+                               {
+                                       ExecComputeStoredGenerated(estate, slot);
+                                       MemoryContextSwitchTo(batchcontext);
+                                       tuple = ExecCopySlotHeapTuple(slot);
+                                       MemoryContextSwitchTo(oldcontext);
+                               }
+
                                /*
                                 * If the target is a plain table, check the constraints of
                                 * the tuple.
@@ -3271,7 +3287,7 @@ BeginCopyFrom(ParseState *pstate,
                fmgr_info(in_func_oid, &in_functions[attnum - 1]);
 
                /* Get default info if needed */
-               if (!list_member_int(cstate->attnumlist, attnum))
+               if (!list_member_int(cstate->attnumlist, attnum) && !att->attgenerated)
                {
                        /* attribute is NOT to be copied from input */
                        /* use default value if one exists */
@@ -4876,6 +4892,11 @@ CopyAttributeOutCSV(CopyState cstate, char *string,
  * or NIL if there was none (in which case we want all the non-dropped
  * columns).
  *
+ * We don't include generated columns in the generated full list and we don't
+ * allow them to be specified explicitly.  They don't make sense for COPY
+ * FROM, but we could possibly allow them for COPY TO.  But this way it's at
+ * least ensured that whatever we copy out can be copied back in.
+ *
  * rel can be NULL ... it's only used for error reports.
  */
 static List *
@@ -4893,6 +4914,8 @@ CopyGetAttnums(TupleDesc tupDesc, Relation rel, List *attnamelist)
                {
                        if (TupleDescAttr(tupDesc, i)->attisdropped)
                                continue;
+                       if (TupleDescAttr(tupDesc, i)->attgenerated)
+                               continue;
                        attnums = lappend_int(attnums, i + 1);
                }
        }
@@ -4917,6 +4940,12 @@ CopyGetAttnums(TupleDesc tupDesc, Relation rel, List *attnamelist)
                                        continue;
                                if (namestrcmp(&(att->attname), name) == 0)
                                {
+                                       if (att->attgenerated)
+                                               ereport(ERROR,
+                                                               (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+                                                                errmsg("column \"%s\" is a generated column",
+                                                                               name),
+                                                                errdetail("Generated columns cannot be used in COPY.")));
                                        attnum = att->attnum;
                                        break;
                                }
index 16492a23c74d1108ab9f362d8c0c32a9a33885b5..31e2b508b8cd8ddf4c1b99992ce308101ced7e79 100644 (file)
@@ -760,6 +760,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
                        rawEnt->attnum = attnum;
                        rawEnt->raw_default = colDef->raw_default;
                        rawEnt->missingMode = false;
+                       rawEnt->generated = colDef->generated;
                        rawDefaults = lappend(rawDefaults, rawEnt);
                        attr->atthasdef = true;
                }
@@ -783,6 +784,9 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 
                if (colDef->identity)
                        attr->attidentity = colDef->identity;
+
+               if (colDef->generated)
+                       attr->attgenerated = colDef->generated;
        }
 
        /*
@@ -863,6 +867,27 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
         */
        rel = relation_open(relationId, AccessExclusiveLock);
 
+       /*
+        * Now add any newly specified column default and generation expressions
+        * to the new relation.  These are passed to us in the form of raw
+        * parsetrees; we need to transform them to executable expression trees
+        * before they can be added. The most convenient way to do that is to
+        * apply the parser's transformExpr routine, but transformExpr doesn't
+        * work unless we have a pre-existing relation. So, the transformation has
+        * to be postponed to this final step of CREATE TABLE.
+        *
+        * This needs to be before processing the partitioning clauses because
+        * those could refer to generated columns.
+        */
+       if (rawDefaults)
+               AddRelationNewConstraints(rel, rawDefaults, NIL,
+                                                                 true, true, false, queryString);
+
+       /*
+        * Make column generation expressions visible for use by partitioning.
+        */
+       CommandCounterIncrement();
+
        /* Process and store partition bound, if any. */
        if (stmt->partbound)
        {
@@ -1064,16 +1089,12 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
        }
 
        /*
-        * Now add any newly specified column default values and CHECK constraints
-        * to the new relation.  These are passed to us in the form of raw
-        * parsetrees; we need to transform them to executable expression trees
-        * before they can be added. The most convenient way to do that is to
-        * apply the parser's transformExpr routine, but transformExpr doesn't
-        * work unless we have a pre-existing relation. So, the transformation has
-        * to be postponed to this final step of CREATE TABLE.
+        * Now add any newly specified CHECK constraints to the new relation.
+        * Same as for defaults above, but these need to come after partitioning
+        * is set up.
         */
-       if (rawDefaults || stmt->constraints)
-               AddRelationNewConstraints(rel, rawDefaults, stmt->constraints,
+       if (stmt->constraints)
+               AddRelationNewConstraints(rel, NIL, stmt->constraints,
                                                                  true, true, false, queryString);
 
        ObjectAddressSet(address, RelationRelationId, relationId);
@@ -2252,6 +2273,13 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
                                def->is_not_null |= attribute->attnotnull;
                                /* Default and other constraints are handled below */
                                newattno[parent_attno - 1] = exist_attno;
+
+                               /* Check for GENERATED conflicts */
+                               if (def->generated != attribute->attgenerated)
+                                       ereport(ERROR,
+                                                       (errcode(ERRCODE_DATATYPE_MISMATCH),
+                                                        errmsg("inherited column \"%s\" has a generation conflict",
+                                                                       attributeName)));
                        }
                        else
                        {
@@ -2269,6 +2297,7 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
                                def->storage = attribute->attstorage;
                                def->raw_default = NULL;
                                def->cooked_default = NULL;
+                               def->generated = attribute->attgenerated;
                                def->collClause = NULL;
                                def->collOid = attribute->attcollation;
                                def->constraints = NIL;
@@ -5613,6 +5642,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
        attribute.atthasdef = false;
        attribute.atthasmissing = false;
        attribute.attidentity = colDef->identity;
+       attribute.attgenerated = colDef->generated;
        attribute.attisdropped = false;
        attribute.attislocal = colDef->is_local;
        attribute.attinhcount = colDef->inhcount;
@@ -5658,7 +5688,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
                 * DEFAULT value outside of the heap.  This may be disabled inside
                 * AddRelationNewConstraints if the optimization cannot be applied.
                 */
-               rawEnt->missingMode = true;
+               rawEnt->missingMode = (!colDef->generated);
+
+               rawEnt->generated = colDef->generated;
 
                /*
                 * This function is intended for CREATE TABLE, so it processes a
@@ -6239,6 +6271,12 @@ ATExecColumnDefault(Relation rel, const char *colName,
                                                colName, RelationGetRelationName(rel)),
                                 newDefault ? 0 : errhint("Use ALTER TABLE ... ALTER COLUMN ... DROP IDENTITY instead.")));
 
+       if (TupleDescAttr(tupdesc, attnum - 1)->attgenerated)
+               ereport(ERROR,
+                               (errcode(ERRCODE_SYNTAX_ERROR),
+                                errmsg("column \"%s\" of relation \"%s\" is a generated column",
+                                               colName, RelationGetRelationName(rel))));
+
        /*
         * Remove any old default for the column.  We use RESTRICT here for
         * safety, but at present we do not expect anything to depend on the
@@ -6260,6 +6298,7 @@ ATExecColumnDefault(Relation rel, const char *colName,
                rawEnt->attnum = attnum;
                rawEnt->raw_default = newDefault;
                rawEnt->missingMode = false;
+               rawEnt->generated = '\0';
 
                /*
                 * This function is intended for CREATE TABLE, so it processes a
@@ -7560,6 +7599,32 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
         */
        checkFkeyPermissions(pkrel, pkattnum, numpks);
 
+       /*
+        * Check some things for generated columns.
+        */
+       for (i = 0; i < numfks; i++)
+       {
+               char            attgenerated = TupleDescAttr(RelationGetDescr(rel), fkattnum[i] - 1)->attgenerated;
+
+               if (attgenerated)
+               {
+                       /*
+                        * Check restrictions on UPDATE/DELETE actions, per SQL standard
+                        */
+                       if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETNULL ||
+                               fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT ||
+                               fkconstraint->fk_upd_action == FKCONSTR_ACTION_CASCADE)
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_SYNTAX_ERROR),
+                                                errmsg("invalid ON UPDATE action for foreign key constraint containing generated column")));
+                       if (fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL ||
+                               fkconstraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT)
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_SYNTAX_ERROR),
+                                                errmsg("invalid ON DELETE action for foreign key constraint containing generated column")));
+               }
+       }
+
        /*
         * Look up the equality operators to use in the constraint.
         *
@@ -9951,10 +10016,18 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
                                                                                        COERCE_IMPLICIT_CAST,
                                                                                        -1);
                if (defaultexpr == NULL)
-                       ereport(ERROR,
-                                       (errcode(ERRCODE_DATATYPE_MISMATCH),
-                                        errmsg("default for column \"%s\" cannot be cast automatically to type %s",
-                                                       colName, format_type_be(targettype))));
+               {
+                       if (attTup->attgenerated)
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_DATATYPE_MISMATCH),
+                                                errmsg("generation expression for column \"%s\" cannot be cast automatically to type %s",
+                                                               colName, format_type_be(targettype))));
+                       else
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_DATATYPE_MISMATCH),
+                                                errmsg("default for column \"%s\" cannot be cast automatically to type %s",
+                                                               colName, format_type_be(targettype))));
+               }
        }
        else
                defaultexpr = NULL;
@@ -10030,6 +10103,21 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
                                                 */
                                                Assert(foundObject.objectSubId == 0);
                                        }
+                                       else if (relKind == RELKIND_RELATION &&
+                                                        foundObject.objectSubId != 0 &&
+                                                        get_attgenerated(foundObject.objectId, foundObject.objectSubId))
+                                       {
+                                               /*
+                                                * Changing the type of a column that is used by a
+                                                * generated column is not allowed by SQL standard.
+                                                * It might be doable with some thinking and effort.
+                                                */
+                                               ereport(ERROR,
+                                                               (errcode(ERRCODE_SYNTAX_ERROR),
+                                                                errmsg("cannot alter type of a column used by a generated column"),
+                                                                errdetail("Column \"%s\" is used by generated column \"%s\".",
+                                                                                  colName, get_attname(foundObject.objectId, foundObject.objectSubId, false))));
+                                       }
                                        else
                                        {
                                                /* Not expecting any other direct dependencies... */
@@ -10174,7 +10262,8 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
        /*
         * Now scan for dependencies of this column on other things.  The only
         * thing we should find is the dependency on the column datatype, which we
-        * want to remove, and possibly a collation dependency.
+        * want to remove, possibly a collation dependency, and dependencies on
+        * other columns if it is a generated column.
         */
        ScanKeyInit(&key[0],
                                Anum_pg_depend_classid,
@@ -10195,15 +10284,26 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
        while (HeapTupleIsValid(depTup = systable_getnext(scan)))
        {
                Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(depTup);
+               ObjectAddress foundObject;
 
-               if (foundDep->deptype != DEPENDENCY_NORMAL)
+               foundObject.classId = foundDep->refclassid;
+               foundObject.objectId = foundDep->refobjid;
+               foundObject.objectSubId = foundDep->refobjsubid;
+
+               if (foundDep->deptype != DEPENDENCY_NORMAL &&
+                       foundDep->deptype != DEPENDENCY_AUTO)
                        elog(ERROR, "found unexpected dependency type '%c'",
                                 foundDep->deptype);
                if (!(foundDep->refclassid == TypeRelationId &&
                          foundDep->refobjid == attTup->atttypid) &&
                        !(foundDep->refclassid == CollationRelationId &&
-                         foundDep->refobjid == attTup->attcollation))
-                       elog(ERROR, "found unexpected dependency for column");
+                         foundDep->refobjid == attTup->attcollation) &&
+                       !(foundDep->refclassid == RelationRelationId &&
+                         foundDep->refobjid == RelationGetRelid(rel) &&
+                         foundDep->refobjsubid != 0)
+                       )
+                       elog(ERROR, "found unexpected dependency for column: %s",
+                                getObjectDescription(&foundObject));
 
                CatalogTupleDelete(depRel, &depTup->t_self);
        }
@@ -14267,6 +14367,18 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
                                                                pelem->name),
                                                 parser_errposition(pstate, pelem->location)));
 
+                       /*
+                        * Generated columns cannot work: They are computed after BEFORE
+                        * triggers, but partition routing is done before all triggers.
+                        */
+                       if (attform->attgenerated)
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                                errmsg("cannot use generated column in partition key"),
+                                                errdetail("Column \"%s\" is a generated column.",
+                                                                  pelem->name),
+                                                parser_errposition(pstate, pelem->location)));
+
                        partattrs[attn] = attform->attnum;
                        atttype = attform->atttypid;
                        attcollation = attform->attcollation;
@@ -14354,6 +14466,25 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
                                                                 errmsg("partition key expressions cannot contain system column references")));
                                }
 
+                               /*
+                                * Generated columns cannot work: They are computed after
+                                * BEFORE triggers, but partition routing is done before all
+                                * triggers.
+                                */
+                               i = -1;
+                               while ((i = bms_next_member(expr_attrs, i)) >= 0)
+                               {
+                                       AttrNumber  attno = i + FirstLowInvalidHeapAttributeNumber;
+
+                                       if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated)
+                                               ereport(ERROR,
+                                                               (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                                                errmsg("cannot use generated column in partition key"),
+                                                                errdetail("Column \"%s\" is a generated column.",
+                                                                                  get_attname(RelationGetRelid(rel), attno, false)),
+                                                                parser_errposition(pstate, pelem->location)));
+                               }
+
                                /*
                                 * While it is not exactly *wrong* for a partition expression
                                 * to be a constant, it seems better to reject such keys.
index e03ffdde3878a4b22d40119c7d95ead0f0c628a8..3ae2640abd4cd9b22b8a2bdad9ed489713101780 100644 (file)
@@ -75,8 +75,9 @@ static int    MyTriggerDepth = 0;
  * they use, so we let them be duplicated.  Be sure to update all if one needs
  * to be changed, however.
  */
-#define GetUpdatedColumns(relinfo, estate) \
-       (exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols)
+#define GetAllUpdatedColumns(relinfo, estate) \
+       (bms_union(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols, \
+                          exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->extraUpdatedCols))
 
 /* Local function prototypes */
 static void ConvertTriggerToFK(CreateTrigStmt *stmt, Oid funcoid);
@@ -640,6 +641,24 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
                                                                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                                                                 errmsg("BEFORE trigger's WHEN condition cannot reference NEW system columns"),
                                                                 parser_errposition(pstate, var->location)));
+                                       if (TRIGGER_FOR_BEFORE(tgtype) &&
+                                               var->varattno == 0 &&
+                                               RelationGetDescr(rel)->constr &&
+                                               RelationGetDescr(rel)->constr->has_generated_stored)
+                                               ereport(ERROR,
+                                                               (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                                                errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
+                                                                errdetail("A whole-row reference is used and the table contains generated columns."),
+                                                                parser_errposition(pstate, var->location)));
+                                       if (TRIGGER_FOR_BEFORE(tgtype) &&
+                                               var->varattno > 0 &&
+                                               TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attgenerated)
+                                               ereport(ERROR,
+                                                               (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                                                errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
+                                                                errdetail("Column \"%s\" is a generated column.",
+                                                                                  NameStr(TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attname)),
+                                                                parser_errposition(pstate, var->location)));
                                        break;
                                default:
                                        /* can't happen without add_missing_from, so just elog */
@@ -2931,7 +2950,7 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
                                                                   CMD_UPDATE))
                return;
 
-       updatedCols = GetUpdatedColumns(relinfo, estate);
+       updatedCols = GetAllUpdatedColumns(relinfo, estate);
 
        LocTriggerData.type = T_TriggerData;
        LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
@@ -2980,7 +2999,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
        if (trigdesc && trigdesc->trig_update_after_statement)
                AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
                                                          false, NULL, NULL, NIL,
-                                                         GetUpdatedColumns(relinfo, estate),
+                                                         GetAllUpdatedColumns(relinfo, estate),
                                                          transition_capture);
 }
 
@@ -3049,7 +3068,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
        LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
        LocTriggerData.tg_oldtable = NULL;
        LocTriggerData.tg_newtable = NULL;
-       updatedCols = GetUpdatedColumns(relinfo, estate);
+       updatedCols = GetAllUpdatedColumns(relinfo, estate);
        for (i = 0; i < trigdesc->numtriggers; i++)
        {
                Trigger    *trigger = &trigdesc->triggers[i];
@@ -3140,7 +3159,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 
                AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
                                                          true, oldslot, newslot, recheckIndexes,
-                                                         GetUpdatedColumns(relinfo, estate),
+                                                         GetAllUpdatedColumns(relinfo, estate),
                                                          transition_capture);
        }
 }
index f94248dc95808b7aedc21317df1813ec68594024..7e6bcc5239c0d63da5ef5567b617e455bd24f9ea 100644 (file)
@@ -918,7 +918,8 @@ DefineDomain(CreateDomainStmt *stmt)
                                        defaultExpr = cookDefault(pstate, constr->raw_expr,
                                                                                          basetypeoid,
                                                                                          basetypeMod,
-                                                                                         domainName);
+                                                                                         domainName,
+                                                                                         0);
 
                                        /*
                                         * If the expression is just a NULL constant, we treat it
@@ -2228,7 +2229,8 @@ AlterDomainDefault(List *names, Node *defaultRaw)
                defaultExpr = cookDefault(pstate, defaultRaw,
                                                                  typTup->typbasetype,
                                                                  typTup->typtypmod,
-                                                                 NameStr(typTup->typname));
+                                                                 NameStr(typTup->typname),
+                                                                 0);
 
                /*
                 * If the expression is just a NULL constant, we treat the command
index 426686b6ef6e8c3140ae255f4d73568e17ede291..03dcc7b820b351eb857ef59aa92cce2d1d34574b 100644 (file)
@@ -102,7 +102,7 @@ static void EvalPlanQualStart(EPQState *epqstate, EState *parentestate,
                                  Plan *planTree);
 
 /*
- * Note that GetUpdatedColumns() also exists in commands/trigger.c.  There does
+ * Note that GetAllUpdatedColumns() also exists in commands/trigger.c.  There does
  * not appear to be any good header to put it into, given the structures that
  * it uses, so we let them be duplicated.  Be sure to update both if one needs
  * to be changed, however.
@@ -111,6 +111,9 @@ static void EvalPlanQualStart(EPQState *epqstate, EState *parentestate,
        (exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->insertedCols)
 #define GetUpdatedColumns(relinfo, estate) \
        (exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols)
+#define GetAllUpdatedColumns(relinfo, estate) \
+       (bms_union(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols, \
+                          exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->extraUpdatedCols))
 
 /* end of local decls */
 
@@ -1316,6 +1319,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
        resultRelInfo->ri_FdwState = NULL;
        resultRelInfo->ri_usesFdwDirectModify = false;
        resultRelInfo->ri_ConstraintExprs = NULL;
+       resultRelInfo->ri_GeneratedExprs = NULL;
        resultRelInfo->ri_junkFilter = NULL;
        resultRelInfo->ri_projectReturning = NULL;
        resultRelInfo->ri_onConflictArbiterIndexes = NIL;
@@ -2328,7 +2332,7 @@ ExecUpdateLockMode(EState *estate, ResultRelInfo *relinfo)
         * been modified, then we can use a weaker lock, allowing for better
         * concurrency.
         */
-       updatedCols = GetUpdatedColumns(relinfo, estate);
+       updatedCols = GetAllUpdatedColumns(relinfo, estate);
        keyCols = RelationGetIndexAttrBitmap(relinfo->ri_RelationDesc,
                                                                                 INDEX_ATTR_BITMAP_KEY);
 
index d8b48c667ce9eb9abad5f430b0be9173499eba86..f8f6463358f8883fe126c2b66515f5565363dd3a 100644 (file)
@@ -21,6 +21,7 @@
 #include "access/xact.h"
 #include "commands/trigger.h"
 #include "executor/executor.h"
+#include "executor/nodeModifyTable.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_relation.h"
 #include "parser/parsetree.h"
@@ -412,6 +413,11 @@ ExecSimpleRelationInsert(EState *estate, TupleTableSlot *slot)
        {
                List       *recheckIndexes = NIL;
 
+               /* Compute stored generated columns */
+               if (rel->rd_att->constr &&
+                       rel->rd_att->constr->has_generated_stored)
+                       ExecComputeStoredGenerated(estate, slot);
+
                /* Check the constraints of the tuple */
                if (rel->rd_att->constr)
                        ExecConstraints(resultRelInfo, slot, estate);
@@ -473,6 +479,11 @@ ExecSimpleRelationUpdate(EState *estate, EPQState *epqstate,
                List       *recheckIndexes = NIL;
                bool            update_indexes;
 
+               /* Compute stored generated columns */
+               if (rel->rd_att->constr &&
+                       rel->rd_att->constr->has_generated_stored)
+                       ExecComputeStoredGenerated(estate, slot);
+
                /* Check the constraints of the tuple */
                if (rel->rd_att->constr)
                        ExecConstraints(resultRelInfo, slot, estate);
index 7be0e7745af298854070ee1d74a430b76c0d8b19..61c4459f676d889ed2d1be2e4f09004908cdf628 100644 (file)
@@ -49,6 +49,7 @@
 #include "foreign/fdwapi.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
+#include "rewrite/rewriteHandler.h"
 #include "storage/bufmgr.h"
 #include "storage/lmgr.h"
 #include "utils/builtins.h"
@@ -240,6 +241,89 @@ ExecCheckTIDVisible(EState *estate,
        ExecClearTuple(tempSlot);
 }
 
+/*
+ * Compute stored generated columns for a tuple
+ */
+void
+ExecComputeStoredGenerated(EState *estate, TupleTableSlot *slot)
+{
+       ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
+       Relation        rel = resultRelInfo->ri_RelationDesc;
+       TupleDesc       tupdesc = RelationGetDescr(rel);
+       int                     natts = tupdesc->natts;
+       MemoryContext oldContext;
+       Datum      *values;
+       bool       *nulls;
+       bool       *replaces;
+       HeapTuple       oldtuple, newtuple;
+       bool            should_free;
+
+       Assert(tupdesc->constr && tupdesc->constr->has_generated_stored);
+
+       /*
+        * If first time through for this result relation, build expression
+        * nodetrees for rel's stored generation expressions.  Keep them in the
+        * per-query memory context so they'll survive throughout the query.
+        */
+       if (resultRelInfo->ri_GeneratedExprs == NULL)
+       {
+               oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
+
+               resultRelInfo->ri_GeneratedExprs =
+                       (ExprState **) palloc(natts * sizeof(ExprState *));
+
+               for (int i = 0; i < natts; i++)
+               {
+                       if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_STORED)
+                       {
+                               Expr       *expr;
+
+                               expr = (Expr *) build_column_default(rel, i + 1);
+                               if (expr == NULL)
+                                       elog(ERROR, "no generation expression found for column number %d of table \"%s\"",
+                                                i + 1, RelationGetRelationName(rel));
+
+                               resultRelInfo->ri_GeneratedExprs[i] = ExecPrepareExpr(expr, estate);
+                       }
+               }
+
+               MemoryContextSwitchTo(oldContext);
+       }
+
+       oldContext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+       values = palloc(sizeof(*values) * natts);
+       nulls = palloc(sizeof(*nulls) * natts);
+       replaces = palloc0(sizeof(*replaces) * natts);
+
+       for (int i = 0; i < natts; i++)
+       {
+               if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_STORED)
+               {
+                       ExprContext *econtext;
+                       Datum           val;
+                       bool            isnull;
+
+                       econtext = GetPerTupleExprContext(estate);
+                       econtext->ecxt_scantuple = slot;
+
+                       val = ExecEvalExpr(resultRelInfo->ri_GeneratedExprs[i], econtext, &isnull);
+
+                       values[i] = val;
+                       nulls[i] = isnull;
+                       replaces[i] = true;
+               }
+       }
+
+       oldtuple = ExecFetchSlotHeapTuple(slot, true, &should_free);
+       newtuple = heap_modify_tuple(oldtuple, tupdesc, values, nulls, replaces);
+       ExecForceStoreHeapTuple(newtuple, slot);
+       if (should_free)
+               heap_freetuple(oldtuple);
+
+       MemoryContextSwitchTo(oldContext);
+}
+
 /* ----------------------------------------------------------------
  *             ExecInsert
  *
@@ -297,6 +381,13 @@ ExecInsert(ModifyTableState *mtstate,
        }
        else if (resultRelInfo->ri_FdwRoutine)
        {
+               /*
+                * Compute stored generated columns
+                */
+               if (resultRelationDesc->rd_att->constr &&
+                       resultRelationDesc->rd_att->constr->has_generated_stored)
+                       ExecComputeStoredGenerated(estate, slot);
+
                /*
                 * insert into foreign table: let the FDW do it
                 */
@@ -326,6 +417,13 @@ ExecInsert(ModifyTableState *mtstate,
                 */
                slot->tts_tableOid = RelationGetRelid(resultRelationDesc);
 
+               /*
+                * Compute stored generated columns
+                */
+               if (resultRelationDesc->rd_att->constr &&
+                       resultRelationDesc->rd_att->constr->has_generated_stored)
+                       ExecComputeStoredGenerated(estate, slot);
+
                /*
                 * Check any RLS WITH CHECK policies.
                 *
@@ -964,6 +1062,13 @@ ExecUpdate(ModifyTableState *mtstate,
        }
        else if (resultRelInfo->ri_FdwRoutine)
        {
+               /*
+                * Compute stored generated columns
+                */
+               if (resultRelationDesc->rd_att->constr &&
+                       resultRelationDesc->rd_att->constr->has_generated_stored)
+                       ExecComputeStoredGenerated(estate, slot);
+
                /*
                 * update in foreign table: let the FDW do it
                 */
@@ -994,6 +1099,13 @@ ExecUpdate(ModifyTableState *mtstate,
                 */
                slot->tts_tableOid = RelationGetRelid(resultRelationDesc);
 
+               /*
+                * Compute stored generated columns
+                */
+               if (resultRelationDesc->rd_att->constr &&
+                       resultRelationDesc->rd_att->constr->has_generated_stored)
+                       ExecComputeStoredGenerated(estate, slot);
+
                /*
                 * Check any RLS UPDATE WITH CHECK policies
                 *
index 84f9112addda8137ae98ef2f5d3a30a1efb71d55..8f51315bee88cc493bfe43b9d6260939a2c812a5 100644 (file)
@@ -2390,6 +2390,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
        COPY_BITMAPSET_FIELD(selectedCols);
        COPY_BITMAPSET_FIELD(insertedCols);
        COPY_BITMAPSET_FIELD(updatedCols);
+       COPY_BITMAPSET_FIELD(extraUpdatedCols);
        COPY_NODE_FIELD(securityQuals);
 
        return newnode;
@@ -2888,6 +2889,7 @@ _copyColumnDef(const ColumnDef *from)
        COPY_NODE_FIELD(cooked_default);
        COPY_SCALAR_FIELD(identity);
        COPY_NODE_FIELD(identitySequence);
+       COPY_SCALAR_FIELD(generated);
        COPY_NODE_FIELD(collClause);
        COPY_SCALAR_FIELD(collOid);
        COPY_NODE_FIELD(constraints);
index 7eb9f1dd928332b8b568cc4075092861000b9e83..68b51f3de7142ebad0c4823d716e7de5d10cbd7d 100644 (file)
@@ -2565,6 +2565,7 @@ _equalColumnDef(const ColumnDef *a, const ColumnDef *b)
        COMPARE_NODE_FIELD(cooked_default);
        COMPARE_SCALAR_FIELD(identity);
        COMPARE_NODE_FIELD(identitySequence);
+       COMPARE_SCALAR_FIELD(generated);
        COMPARE_NODE_FIELD(collClause);
        COMPARE_SCALAR_FIELD(collOid);
        COMPARE_NODE_FIELD(constraints);
@@ -2664,6 +2665,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
        COMPARE_BITMAPSET_FIELD(selectedCols);
        COMPARE_BITMAPSET_FIELD(insertedCols);
        COMPARE_BITMAPSET_FIELD(updatedCols);
+       COMPARE_BITMAPSET_FIELD(extraUpdatedCols);
        COMPARE_NODE_FIELD(securityQuals);
 
        return true;
index 910a738c205de69d869d122c25e642af20947b30..3282be0e4bd56eb4de55934a009043e7d779c87e 100644 (file)
@@ -2792,6 +2792,7 @@ _outColumnDef(StringInfo str, const ColumnDef *node)
        WRITE_NODE_FIELD(cooked_default);
        WRITE_CHAR_FIELD(identity);
        WRITE_NODE_FIELD(identitySequence);
+       WRITE_CHAR_FIELD(generated);
        WRITE_NODE_FIELD(collClause);
        WRITE_OID_FIELD(collOid);
        WRITE_NODE_FIELD(constraints);
@@ -3096,6 +3097,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
        WRITE_BITMAPSET_FIELD(selectedCols);
        WRITE_BITMAPSET_FIELD(insertedCols);
        WRITE_BITMAPSET_FIELD(updatedCols);
+       WRITE_BITMAPSET_FIELD(extraUpdatedCols);
        WRITE_NODE_FIELD(securityQuals);
 }
 
@@ -3467,6 +3469,13 @@ _outConstraint(StringInfo str, const Constraint *node)
                        WRITE_CHAR_FIELD(generated_when);
                        break;
 
+               case CONSTR_GENERATED:
+                       appendStringInfoString(str, "GENERATED");
+                       WRITE_NODE_FIELD(raw_expr);
+                       WRITE_STRING_FIELD(cooked_expr);
+                       WRITE_CHAR_FIELD(generated_when);
+                       break;
+
                case CONSTR_CHECK:
                        appendStringInfoString(str, "CHECK");
                        WRITE_BOOL_FIELD(is_no_inherit);
index eff98febf1fa80748acf50e1b8817b1ddbaee9ab..3b96492b3679666cf4129beb1f0c1fc568f3a82a 100644 (file)
@@ -1430,6 +1430,7 @@ _readRangeTblEntry(void)
        READ_BITMAPSET_FIELD(selectedCols);
        READ_BITMAPSET_FIELD(insertedCols);
        READ_BITMAPSET_FIELD(updatedCols);
+       READ_BITMAPSET_FIELD(extraUpdatedCols);
        READ_NODE_FIELD(securityQuals);
 
        READ_DONE();
index 979c3c212fd2c78074b9ed59e2ebba7e6cfb968e..cc222cb06cf309000f005a44926fbf501406ed07 100644 (file)
@@ -6570,8 +6570,9 @@ make_modifytable(PlannerInfo *root,
 
                /*
                 * Try to modify the foreign table directly if (1) the FDW provides
-                * callback functions needed for that, (2) there are no row-level
-                * triggers on the foreign table, and (3) there are no WITH CHECK
+                * callback functions needed for that and (2) there are no local
+                * structures that need to be run for each modified row: row-level
+                * triggers on the foreign table, stored generated columns, WITH CHECK
                 * OPTIONs from parent views.
                 */
                direct_modify = false;
@@ -6581,7 +6582,8 @@ make_modifytable(PlannerInfo *root,
                        fdwroutine->IterateDirectModify != NULL &&
                        fdwroutine->EndDirectModify != NULL &&
                        withCheckOptionLists == NIL &&
-                       !has_row_triggers(subroot, rti, operation))
+                       !has_row_triggers(subroot, rti, operation) &&
+                       !has_stored_generated_columns(subroot, rti))
                        direct_modify = fdwroutine->PlanDirectModify(subroot, node, rti, i);
                if (direct_modify)
                        direct_modify_plans = bms_add_member(direct_modify_plans, i);
index 1d1e5060e26af41d98c227862227057a3c7d0f06..d467a4826b0671a93d92337276bf999949afbc63 100644 (file)
@@ -280,6 +280,10 @@ expand_partitioned_rtentry(PlannerInfo *root, RangeTblEntry *parentrte,
        if (!root->partColsUpdated)
                root->partColsUpdated =
                        has_partition_attrs(parentrel, parentrte->updatedCols, NULL);
+       /*
+        * There shouldn't be any generated columns in the partition key.
+        */
+       Assert(!has_partition_attrs(parentrel, parentrte->extraUpdatedCols, NULL));
 
        /*
         * If the partitioned table has no partitions, treat this as the
@@ -415,6 +419,8 @@ expand_single_inheritance_child(PlannerInfo *root, RangeTblEntry *parentrte,
                                                                                                         appinfo->translated_vars);
                childrte->updatedCols = translate_col_privs(parentrte->updatedCols,
                                                                                                        appinfo->translated_vars);
+               childrte->extraUpdatedCols = translate_col_privs(parentrte->extraUpdatedCols,
+                                                                                                                appinfo->translated_vars);
        }
 
        /*
index a0a7c5442de70e7e75af1bbc354990b437db552f..2ebded8a95411483cd2d87b778c68752bca6b907 100644 (file)
@@ -2083,6 +2083,25 @@ has_row_triggers(PlannerInfo *root, Index rti, CmdType event)
        return result;
 }
 
+bool
+has_stored_generated_columns(PlannerInfo *root, Index rti)
+{
+       RangeTblEntry *rte = planner_rt_fetch(rti, root);
+       Relation    relation;
+       TupleDesc       tupdesc;
+       bool        result = false;
+
+       /* Assume we already have adequate lock */
+       relation = heap_open(rte->relid, NoLock);
+
+       tupdesc = RelationGetDescr(relation);
+       result = tupdesc->constr && tupdesc->constr->has_generated_stored;
+
+       heap_close(relation, NoLock);
+
+       return result;
+}
+
 /*
  * set_relation_partition_info
  *
index d6cdd1660739181e9e1bd50646f622105cea5792..400558b552badfa9825afd70d8899d9b5e9c47db 100644 (file)
@@ -2287,6 +2287,7 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
        RangeTblEntry *target_rte;
        ListCell   *orig_tl;
        ListCell   *tl;
+       TupleDesc       tupdesc = pstate->p_target_relation->rd_att;
 
        tlist = transformTargetList(pstate, origTlist,
                                                                EXPR_KIND_UPDATE_SOURCE);
@@ -2345,6 +2346,32 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
        if (orig_tl != NULL)
                elog(ERROR, "UPDATE target count mismatch --- internal error");
 
+       /*
+        * Record in extraUpdatedCols generated columns referencing updated base
+        * columns.
+        */
+       if (tupdesc->constr &&
+               tupdesc->constr->has_generated_stored)
+       {
+               for (int i = 0; i < tupdesc->constr->num_defval; i++)
+               {
+                       AttrDefault defval = tupdesc->constr->defval[i];
+                       Node       *expr;
+                       Bitmapset  *attrs_used = NULL;
+
+                       /* skip if not generated column */
+                       if (!TupleDescAttr(tupdesc, defval.adnum - 1)->attgenerated)
+                               continue;
+
+                       expr = stringToNode(defval.adbin);
+                       pull_varattnos(expr, 1, &attrs_used);
+
+                       if (bms_overlap(target_rte->updatedCols, attrs_used))
+                               target_rte->extraUpdatedCols = bms_add_member(target_rte->extraUpdatedCols,
+                                                                                                                         defval.adnum - FirstLowInvalidHeapAttributeNumber);
+               }
+       }
+
        return tlist;
 }
 
index 28f62de97e52ce72c0e18d10e99230282cf2bc63..01521789e82e5fd8668693c5c904ec179d9bb004 100644 (file)
@@ -681,7 +681,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
        SAVEPOINT SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES
        SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
        SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P
-       START STATEMENT STATISTICS STDIN STDOUT STORAGE STRICT_P STRIP_P
+       START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P
        SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P
 
        TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN
@@ -3497,6 +3497,16 @@ ColConstraintElem:
                                        n->location = @1;
                                        $$ = (Node *)n;
                                }
+                       | GENERATED generated_when AS '(' a_expr ')' STORED
+                               {
+                                       Constraint *n = makeNode(Constraint);
+                                       n->contype = CONSTR_GENERATED;
+                                       n->generated_when = $2;
+                                       n->raw_expr = $5;
+                                       n->cooked_expr = NULL;
+                                       n->location = @1;
+                                       $$ = (Node *)n;
+                               }
                        | REFERENCES qualified_name opt_column_list key_match key_actions
                                {
                                        Constraint *n = makeNode(Constraint);
@@ -3587,6 +3597,7 @@ TableLikeOption:
                                | CONSTRAINTS           { $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
                                | DEFAULTS                      { $$ = CREATE_TABLE_LIKE_DEFAULTS; }
                                | IDENTITY_P            { $$ = CREATE_TABLE_LIKE_IDENTITY; }
+                               | GENERATED                     { $$ = CREATE_TABLE_LIKE_GENERATED; }
                                | INDEXES                       { $$ = CREATE_TABLE_LIKE_INDEXES; }
                                | STATISTICS            { $$ = CREATE_TABLE_LIKE_STATISTICS; }
                                | STORAGE                       { $$ = CREATE_TABLE_LIKE_STORAGE; }
@@ -15234,6 +15245,7 @@ unreserved_keyword:
                        | STDIN
                        | STDOUT
                        | STORAGE
+                       | STORED
                        | STRICT_P
                        | STRIP_P
                        | SUBSCRIPTION
index 183ea0f2c4a33e8fea263223900763a3a3a3edff..c745fcdd2b73c51397a2061ace572a727f17b078 100644 (file)
@@ -520,6 +520,14 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
                                err = _("grouping operations are not allowed in partition key expressions");
 
                        break;
+               case EXPR_KIND_GENERATED_COLUMN:
+
+                       if (isAgg)
+                               err = _("aggregate functions are not allowed in column generation expressions");
+                       else
+                               err = _("grouping operations are not allowed in column generation expressions");
+
+                       break;
 
                case EXPR_KIND_CALL_ARGUMENT:
                        if (isAgg)
@@ -922,6 +930,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
                case EXPR_KIND_COPY_WHERE:
                        err = _("window functions are not allowed in COPY FROM WHERE conditions");
                        break;
+               case EXPR_KIND_GENERATED_COLUMN:
+                       err = _("window functions are not allowed in column generation expressions");
+                       break;
 
                        /*
                         * There is intentionally no default: case here, so that the
index 3e648dc8ef9351f8522b15945051f87b1ae7fb29..20d923203f4473252bc8468aad977b254ca8de19 100644 (file)
@@ -570,6 +570,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
                case EXPR_KIND_PARTITION_EXPRESSION:
                case EXPR_KIND_CALL_ARGUMENT:
                case EXPR_KIND_COPY_WHERE:
+               case EXPR_KIND_GENERATED_COLUMN:
                        /* okay */
                        break;
 
@@ -1927,6 +1928,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
                case EXPR_KIND_COPY_WHERE:
                        err = _("cannot use subquery in COPY FROM WHERE condition");
                        break;
+               case EXPR_KIND_GENERATED_COLUMN:
+                       err = _("cannot use subquery in column generation expression");
+                       break;
 
                        /*
                         * There is intentionally no default: case here, so that the
@@ -3557,6 +3561,8 @@ ParseExprKindName(ParseExprKind exprKind)
                        return "CALL";
                case EXPR_KIND_COPY_WHERE:
                        return "WHERE";
+               case EXPR_KIND_GENERATED_COLUMN:
+                       return "GENERATED AS";
 
                        /*
                         * There is intentionally no default: case here, so that the
index b8447771bd99d282a6911d93b651532374eeeb7a..752cf1b315f94ac1554aee92a88f8add8aee4bd0 100644 (file)
@@ -2526,6 +2526,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
                case EXPR_KIND_COPY_WHERE:
                        err = _("set-returning functions are not allowed in COPY FROM WHERE conditions");
                        break;
+               case EXPR_KIND_GENERATED_COLUMN:
+                       err = _("set-returning functions are not allowed in column generation expressions");
+                       break;
 
                        /*
                         * There is intentionally no default: case here, so that the
index f3b6d193aa763e5e1fc8f833d4acbfc95ad4a244..0640d11fac7c089ee14a10481250dd1a8487e9fe 100644 (file)
@@ -731,6 +731,17 @@ scanRTEForColumn(ParseState *pstate, RangeTblEntry *rte, const char *colname,
                                                        colname),
                                         parser_errposition(pstate, location)));
 
+               /*
+                * In generated column, no system column is allowed except tableOid.
+                */
+               if (pstate->p_expr_kind == EXPR_KIND_GENERATED_COLUMN &&
+                       attnum < InvalidAttrNumber && attnum != TableOidAttributeNumber)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+                                        errmsg("cannot use system column \"%s\" in column generation expression",
+                                                       colname),
+                                        parser_errposition(pstate, location)));
+
                if (attnum != InvalidAttrNumber)
                {
                        /* now check to see if column actually is defined */
@@ -1257,6 +1268,7 @@ addRangeTableEntry(ParseState *pstate,
        rte->selectedCols = NULL;
        rte->insertedCols = NULL;
        rte->updatedCols = NULL;
+       rte->extraUpdatedCols = NULL;
 
        /*
         * Add completed RTE to pstate's range table list, but not to join list
@@ -1328,6 +1340,7 @@ addRangeTableEntryForRelation(ParseState *pstate,
        rte->selectedCols = NULL;
        rte->insertedCols = NULL;
        rte->updatedCols = NULL;
+       rte->extraUpdatedCols = NULL;
 
        /*
         * Add completed RTE to pstate's range table list, but not to join list
@@ -1407,6 +1420,7 @@ addRangeTableEntryForSubquery(ParseState *pstate,
        rte->selectedCols = NULL;
        rte->insertedCols = NULL;
        rte->updatedCols = NULL;
+       rte->extraUpdatedCols = NULL;
 
        /*
         * Add completed RTE to pstate's range table list, but not to join list
@@ -1670,6 +1684,7 @@ addRangeTableEntryForFunction(ParseState *pstate,
        rte->selectedCols = NULL;
        rte->insertedCols = NULL;
        rte->updatedCols = NULL;
+       rte->extraUpdatedCols = NULL;
 
        /*
         * Add completed RTE to pstate's range table list, but not to join list
@@ -1733,6 +1748,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate,
        rte->selectedCols = NULL;
        rte->insertedCols = NULL;
        rte->updatedCols = NULL;
+       rte->extraUpdatedCols = NULL;
 
        /*
         * Add completed RTE to pstate's range table list, but not to join list
@@ -1811,6 +1827,7 @@ addRangeTableEntryForValues(ParseState *pstate,
        rte->selectedCols = NULL;
        rte->insertedCols = NULL;
        rte->updatedCols = NULL;
+       rte->extraUpdatedCols = NULL;
 
        /*
         * Add completed RTE to pstate's range table list, but not to join list
@@ -1881,6 +1898,7 @@ addRangeTableEntryForJoin(ParseState *pstate,
        rte->selectedCols = NULL;
        rte->insertedCols = NULL;
        rte->updatedCols = NULL;
+       rte->extraUpdatedCols = NULL;
 
        /*
         * Add completed RTE to pstate's range table list, but not to join list
@@ -1983,6 +2001,7 @@ addRangeTableEntryForCTE(ParseState *pstate,
        rte->selectedCols = NULL;
        rte->insertedCols = NULL;
        rte->updatedCols = NULL;
+       rte->extraUpdatedCols = NULL;
 
        /*
         * Add completed RTE to pstate's range table list, but not to join list
index 8fdd3d785c7fd915828fbd2d8951bdea0ca83dec..674f4b98f401dba830e9baa83b36d4bd26245d69 100644 (file)
@@ -502,6 +502,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
        bool            saw_nullable;
        bool            saw_default;
        bool            saw_identity;
+       bool            saw_generated;
        ListCell   *clist;
 
        cxt->columns = lappend(cxt->columns, column);
@@ -609,6 +610,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
        saw_nullable = false;
        saw_default = false;
        saw_identity = false;
+       saw_generated = false;
 
        foreach(clist, column->constraints)
        {
@@ -689,6 +691,29 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
                                        break;
                                }
 
+                       case CONSTR_GENERATED:
+                               if (cxt->ofType)
+                                       ereport(ERROR,
+                                                       (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                                        errmsg("generated columns are not supported on typed tables")));
+                               if (cxt->partbound)
+                                       ereport(ERROR,
+                                                       (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                                        errmsg("generated columns are not supported on partitions")));
+
+                               if (saw_generated)
+                                       ereport(ERROR,
+                                                       (errcode(ERRCODE_SYNTAX_ERROR),
+                                                        errmsg("multiple generation clauses specified for column \"%s\" of table \"%s\"",
+                                                                       column->colname, cxt->relation->relname),
+                                                        parser_errposition(cxt->pstate,
+                                                                                               constraint->location)));
+                               column->generated = ATTRIBUTE_GENERATED_STORED;
+                               column->raw_default = constraint->raw_expr;
+                               Assert(constraint->cooked_expr == NULL);
+                               saw_generated = true;
+                               break;
+
                        case CONSTR_CHECK:
                                cxt->ckconstraints = lappend(cxt->ckconstraints, constraint);
                                break;
@@ -755,6 +780,22 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
                                                        column->colname, cxt->relation->relname),
                                         parser_errposition(cxt->pstate,
                                                                                constraint->location)));
+
+               if (saw_default && saw_generated)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_SYNTAX_ERROR),
+                                        errmsg("both default and generation expression specified for column \"%s\" of table \"%s\"",
+                                                       column->colname, cxt->relation->relname),
+                                        parser_errposition(cxt->pstate,
+                                                                               constraint->location)));
+
+               if (saw_identity && saw_generated)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_SYNTAX_ERROR),
+                                        errmsg("both identity and generation expression specified for column \"%s\" of table \"%s\"",
+                                                       column->colname, cxt->relation->relname),
+                                        parser_errposition(cxt->pstate,
+                                                                               constraint->location)));
        }
 
        /*
@@ -983,11 +1024,13 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
                 * Copy default, if present and the default has been requested
                 */
                if (attribute->atthasdef &&
-                       (table_like_clause->options & CREATE_TABLE_LIKE_DEFAULTS))
+                       (table_like_clause->options & CREATE_TABLE_LIKE_DEFAULTS ||
+                        table_like_clause->options & CREATE_TABLE_LIKE_GENERATED))
                {
                        Node       *this_default = NULL;
                        AttrDefault *attrdef;
                        int                     i;
+                       bool            found_whole_row;
 
                        /* Find default in constraint structure */
                        Assert(constr != NULL);
@@ -1002,12 +1045,27 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
                        }
                        Assert(this_default != NULL);
 
+                       def->cooked_default = map_variable_attnos(this_default,
+                                                                                                         1, 0,
+                                                                                                         attmap, tupleDesc->natts,
+                                                                                                         InvalidOid, &found_whole_row);
+
                        /*
-                        * If default expr could contain any vars, we'd need to fix 'em,
-                        * but it can't; so default is ready to apply to child.
+                        * Prevent this for the same reason as for constraints below.
+                        * Note that defaults cannot contain any vars, so it's OK that the
+                        * error message refers to generated columns.
                         */
+                       if (found_whole_row)
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                                errmsg("cannot convert whole-row table reference"),
+                                                errdetail("Generation expression for column \"%s\" contains a whole-row reference to table \"%s\".",
+                                                                  attributeName,
+                                                                  RelationGetRelationName(relation))));
 
-                       def->cooked_default = this_default;
+                       if (attribute->attgenerated &&
+                               (table_like_clause->options & CREATE_TABLE_LIKE_GENERATED))
+                               def->generated = attribute->attgenerated;
                }
 
                /*
index dffb6cd9fd61c9e6d6f178b2d8183ee377475ff5..0411963f939c795f5a8b8643e9589f1f23e0d306 100644 (file)
@@ -453,7 +453,7 @@ logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple)
 
        for (i = 0; i < desc->natts; i++)
        {
-               if (TupleDescAttr(desc, i)->attisdropped)
+               if (TupleDescAttr(desc, i)->attisdropped || TupleDescAttr(desc, i)->attgenerated)
                        continue;
                nliveatts++;
        }
@@ -473,8 +473,7 @@ logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple)
                Form_pg_attribute att = TupleDescAttr(desc, i);
                char       *outputstr;
 
-               /* skip dropped columns */
-               if (att->attisdropped)
+               if (att->attisdropped || att->attgenerated)
                        continue;
 
                if (isnull[i])
@@ -573,7 +572,7 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
        /* send number of live attributes */
        for (i = 0; i < desc->natts; i++)
        {
-               if (TupleDescAttr(desc, i)->attisdropped)
+               if (TupleDescAttr(desc, i)->attisdropped || TupleDescAttr(desc, i)->attgenerated)
                        continue;
                nliveatts++;
        }
@@ -591,7 +590,7 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
                Form_pg_attribute att = TupleDescAttr(desc, i);
                uint8           flags = 0;
 
-               if (att->attisdropped)
+               if (att->attisdropped || att->attgenerated)
                        continue;
 
                /* REPLICA IDENTITY FULL means all columns are sent as part of key. */
index 1d918d2c428c68218442bc9ef63b9b65be6838e7..5aee4b80e66096171f5602edbfc3484878acb3a3 100644 (file)
@@ -276,7 +276,7 @@ logicalrep_rel_open(LogicalRepRelId remoteid, LOCKMODE lockmode)
                        int                     attnum;
                        Form_pg_attribute attr = TupleDescAttr(desc, i);
 
-                       if (attr->attisdropped)
+                       if (attr->attisdropped || attr->attgenerated)
                        {
                                entry->attrmap[i] = -1;
                                continue;
index 28f5fc23aacc6cfb563176aaa9ef732cd0e278fd..7881079e96b2d49c9c7f2d5e1bd6520d59814886 100644 (file)
@@ -697,10 +697,12 @@ fetch_remote_table_info(char *nspname, char *relname,
                                         "  LEFT JOIN pg_catalog.pg_index i"
                                         "       ON (i.indexrelid = pg_get_replica_identity_index(%u))"
                                         " WHERE a.attnum > 0::pg_catalog.int2"
-                                        "   AND NOT a.attisdropped"
+                                        "   AND NOT a.attisdropped %s"
                                         "   AND a.attrelid = %u"
                                         " ORDER BY a.attnum",
-                                        lrel->remoteid, lrel->remoteid);
+                                        lrel->remoteid,
+                                        (walrcv_server_version(wrconn) >= 120000 ? "AND a.attgenerated = ''" : ""),
+                                        lrel->remoteid);
        res = walrcv_exec(wrconn, cmd.data, 4, attrRow);
 
        if (res->status != WALRCV_OK_TUPLES)
index 52a5090b6944d98622f0d8000793bde9c8951f4c..43edfef0895db6113902567f45f3a5e8f8066508 100644 (file)
@@ -236,7 +236,7 @@ slot_fill_defaults(LogicalRepRelMapEntry *rel, EState *estate,
        {
                Expr       *defexpr;
 
-               if (TupleDescAttr(desc, attnum)->attisdropped)
+               if (TupleDescAttr(desc, attnum)->attisdropped || TupleDescAttr(desc, attnum)->attgenerated)
                        continue;
 
                if (rel->attrmap[attnum] >= 0)
index 5511957516406c64322b8d93413945c756d021f6..bf64c8e4a42a35fda8c68823b3c357a1fb3d279a 100644 (file)
@@ -276,7 +276,7 @@ maybe_send_schema(LogicalDecodingContext *ctx,
                {
                        Form_pg_attribute att = TupleDescAttr(desc, i);
 
-                       if (att->attisdropped)
+                       if (att->attisdropped || att->attgenerated)
                                continue;
 
                        if (att->atttypid < FirstNormalObjectId)
index 4fc50c89b92f2d04d4b84dacec8913f68f041c0f..39080776b04647863790dc9a8aaa522bd35ce876 100644 (file)
@@ -818,6 +818,13 @@ rewriteTargetListIU(List *targetList,
 
                        if (att_tup->attidentity == ATTRIBUTE_IDENTITY_BY_DEFAULT && override == OVERRIDING_USER_VALUE)
                                apply_default = true;
+
+                       if (att_tup->attgenerated && !apply_default)
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_SYNTAX_ERROR),
+                                                errmsg("cannot insert into column \"%s\"", NameStr(att_tup->attname)),
+                                                errdetail("Column \"%s\" is a generated column.",
+                                                                  NameStr(att_tup->attname))));
                }
 
                if (commandType == CMD_UPDATE)
@@ -828,9 +835,23 @@ rewriteTargetListIU(List *targetList,
                                                 errmsg("column \"%s\" can only be updated to DEFAULT", NameStr(att_tup->attname)),
                                                 errdetail("Column \"%s\" is an identity column defined as GENERATED ALWAYS.",
                                                                   NameStr(att_tup->attname))));
+
+                       if (att_tup->attgenerated && new_tle && !apply_default)
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_SYNTAX_ERROR),
+                                                errmsg("column \"%s\" can only be updated to DEFAULT", NameStr(att_tup->attname)),
+                                                errdetail("Column \"%s\" is a generated column.",
+                                                                  NameStr(att_tup->attname))));
                }
 
-               if (apply_default)
+               if (att_tup->attgenerated)
+               {
+                       /*
+                        * stored generated column will be fixed in executor
+                        */
+                       new_tle = NULL;
+               }
+               else if (apply_default)
                {
                        Node       *new_expr;
 
@@ -1137,13 +1158,12 @@ build_column_default(Relation rel, int attrno)
                }
        }
 
-       if (expr == NULL)
-       {
-               /*
-                * No per-column default, so look for a default for the type itself.
-                */
+       /*
+        * No per-column default, so look for a default for the type itself.  But
+        * not for generated columns.
+        */
+       if (expr == NULL && !att_tup->attgenerated)
                expr = get_typdefault(atttype);
-       }
 
        if (expr == NULL)
                return NULL;                    /* No default anywhere */
@@ -1720,12 +1740,14 @@ ApplyRetrieveRule(Query *parsetree,
        subrte->selectedCols = rte->selectedCols;
        subrte->insertedCols = rte->insertedCols;
        subrte->updatedCols = rte->updatedCols;
+       subrte->extraUpdatedCols = rte->extraUpdatedCols;
 
        rte->requiredPerms = 0;         /* no permission check on subquery itself */
        rte->checkAsUser = InvalidOid;
        rte->selectedCols = NULL;
        rte->insertedCols = NULL;
        rte->updatedCols = NULL;
+       rte->extraUpdatedCols = NULL;
 
        return parsetree;
 }
index 59e6bcd856cece7528cb22c10e43263ffbe5a941..10895567c09d0c1ec971cbd43c32f0c373f8f2a2 100644 (file)
@@ -821,6 +821,39 @@ get_attnum(Oid relid, const char *attname)
                return InvalidAttrNumber;
 }
 
+/*
+ * get_attgenerated
+ *
+ *             Given the relation id and the attribute name,
+ *             return the "attgenerated" field from the attribute relation.
+ *
+ *             Errors if not found.
+ *
+ *             Since not generated is represented by '\0', this can also be used as a
+ *             Boolean test.
+ */
+char
+get_attgenerated(Oid relid, AttrNumber attnum)
+{
+       HeapTuple       tp;
+
+       tp = SearchSysCache2(ATTNUM,
+                                                ObjectIdGetDatum(relid),
+                                                Int16GetDatum(attnum));
+       if (HeapTupleIsValid(tp))
+       {
+               Form_pg_attribute att_tup = (Form_pg_attribute) GETSTRUCT(tp);
+               char            result;
+
+               result = att_tup->attgenerated;
+               ReleaseSysCache(tp);
+               return result;
+       }
+       else
+               elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+                        attnum, relid);
+}
+
 /*
  * get_atttype
  *
index 2b55f25e758b9f781a12b3a9e809e56670e26827..8f43d682cf3d207dd14f510dc34114b7cd51565c 100644 (file)
@@ -27,6 +27,7 @@
 #include "nodes/nodeFuncs.h"
 #include "optimizer/optimizer.h"
 #include "partitioning/partbounds.h"
+#include "rewrite/rewriteHandler.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
 #include "utils/lsyscache.h"
index 12f813f0bc5f372c9b4b91fe46db0659a522c07a..64f3c2e88701ba7d9f252c3a7adfa5c9c34d866b 100644 (file)
@@ -515,6 +515,7 @@ RelationBuildTupleDesc(Relation relation)
        constr = (TupleConstr *) MemoryContextAlloc(CacheMemoryContext,
                                                                                                sizeof(TupleConstr));
        constr->has_not_null = false;
+       constr->has_generated_stored = false;
 
        /*
         * Form a scan key that selects only user attributes (attnum > 0).
@@ -567,6 +568,8 @@ RelationBuildTupleDesc(Relation relation)
                /* Update constraint/default info */
                if (attp->attnotnull)
                        constr->has_not_null = true;
+               if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
+                       constr->has_generated_stored = true;
 
                /* If the column has a default, fill it into the attrdef array */
                if (attp->atthasdef)
@@ -3281,6 +3284,7 @@ RelationBuildLocalRelation(const char *relname,
                Form_pg_attribute datt = TupleDescAttr(rel->rd_att, i);
 
                datt->attidentity = satt->attidentity;
+               datt->attgenerated = satt->attgenerated;
                datt->attnotnull = satt->attnotnull;
                has_not_null |= satt->attnotnull;
        }
index 63699932c15d3a1cbb9e3a75ad91786acda91f1c..2a9e8538c62954288ace1db371e4e6ba44da6e2c 100644 (file)
@@ -2051,6 +2051,11 @@ dumpTableData_insert(Archive *fout, void *dcontext)
                        {
                                if (field > 0)
                                        archputs(", ", fout);
+                               if (tbinfo->attgenerated[field])
+                               {
+                                       archputs("DEFAULT", fout);
+                                       continue;
+                               }
                                if (PQgetisnull(res, tuple, field))
                                {
                                        archputs("NULL", fout);
@@ -8219,6 +8224,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
        int                     i_attnotnull;
        int                     i_atthasdef;
        int                     i_attidentity;
+       int                     i_attgenerated;
        int                     i_attisdropped;
        int                     i_attlen;
        int                     i_attalign;
@@ -8272,6 +8278,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
                                                  "a.attislocal,\n"
                                                  "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n");
 
+               if (fout->remoteVersion >= 120000)
+                       appendPQExpBuffer(q,
+                                                         "a.attgenerated,\n");
+               else
+                       appendPQExpBuffer(q,
+                                                         "'' AS attgenerated,\n");
+
                if (fout->remoteVersion >= 110000)
                        appendPQExpBuffer(q,
                                                          "CASE WHEN a.atthasmissing AND NOT a.attisdropped "
@@ -8344,6 +8357,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
                i_attnotnull = PQfnumber(res, "attnotnull");
                i_atthasdef = PQfnumber(res, "atthasdef");
                i_attidentity = PQfnumber(res, "attidentity");
+               i_attgenerated = PQfnumber(res, "attgenerated");
                i_attisdropped = PQfnumber(res, "attisdropped");
                i_attlen = PQfnumber(res, "attlen");
                i_attalign = PQfnumber(res, "attalign");
@@ -8361,6 +8375,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
                tbinfo->attstorage = (char *) pg_malloc(ntups * sizeof(char));
                tbinfo->typstorage = (char *) pg_malloc(ntups * sizeof(char));
                tbinfo->attidentity = (char *) pg_malloc(ntups * sizeof(char));
+               tbinfo->attgenerated = (char *) pg_malloc(ntups * sizeof(char));
                tbinfo->attisdropped = (bool *) pg_malloc(ntups * sizeof(bool));
                tbinfo->attlen = (int *) pg_malloc(ntups * sizeof(int));
                tbinfo->attalign = (char *) pg_malloc(ntups * sizeof(char));
@@ -8387,6 +8402,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
                        tbinfo->attstorage[j] = *(PQgetvalue(res, j, i_attstorage));
                        tbinfo->typstorage[j] = *(PQgetvalue(res, j, i_typstorage));
                        tbinfo->attidentity[j] = *(PQgetvalue(res, j, i_attidentity));
+                       tbinfo->attgenerated[j] = *(PQgetvalue(res, j, i_attgenerated));
                        tbinfo->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS);
                        tbinfo->attisdropped[j] = (PQgetvalue(res, j, i_attisdropped)[0] == 't');
                        tbinfo->attlen[j] = atoi(PQgetvalue(res, j, i_attlen));
@@ -15708,6 +15724,20 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
                                                                                  tbinfo->atttypnames[j]);
                                        }
 
+                                       if (has_default)
+                                       {
+                                               if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
+                                                       appendPQExpBuffer(q, " GENERATED ALWAYS AS (%s) STORED",
+                                                                                         tbinfo->attrdefs[j]->adef_expr);
+                                               else
+                                                       appendPQExpBuffer(q, " DEFAULT %s",
+                                                                                         tbinfo->attrdefs[j]->adef_expr);
+                                       }
+
+
+                                       if (has_notnull)
+                                               appendPQExpBufferStr(q, " NOT NULL");
+
                                        /* Add collation if not default for the type */
                                        if (OidIsValid(tbinfo->attcollation[j]))
                                        {
@@ -15718,13 +15748,6 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
                                                        appendPQExpBuffer(q, " COLLATE %s",
                                                                                          fmtQualifiedDumpable(coll));
                                        }
-
-                                       if (has_default)
-                                               appendPQExpBuffer(q, " DEFAULT %s",
-                                                                                 tbinfo->attrdefs[j]->adef_expr);
-
-                                       if (has_notnull)
-                                               appendPQExpBufferStr(q, " NOT NULL");
                                }
                        }
 
@@ -18303,6 +18326,7 @@ fmtCopyColumnList(const TableInfo *ti, PQExpBuffer buffer)
        int                     numatts = ti->numatts;
        char      **attnames = ti->attnames;
        bool       *attisdropped = ti->attisdropped;
+       char       *attgenerated = ti->attgenerated;
        bool            needComma;
        int                     i;
 
@@ -18312,6 +18336,8 @@ fmtCopyColumnList(const TableInfo *ti, PQExpBuffer buffer)
        {
                if (attisdropped[i])
                        continue;
+               if (attgenerated[i])
+                       continue;
                if (needComma)
                        appendPQExpBufferStr(buffer, ", ");
                appendPQExpBufferStr(buffer, fmtId(attnames[i]));
index 2e1b90acd0a2f20e5a5d661760d1736a3524952e..a72e3eb27c5a9ebccb71c531965d2a52bb097991 100644 (file)
@@ -310,6 +310,7 @@ typedef struct _tableInfo
        char       *typstorage;         /* type storage scheme */
        bool       *attisdropped;       /* true if attr is dropped; don't dump it */
        char       *attidentity;
+       char       *attgenerated;
        int                *attlen;                     /* attribute length, used by binary_upgrade */
        char       *attalign;           /* attribute align, used by binary_upgrade */
        bool       *attislocal;         /* true if attr has local definition */
index a7a5b1e7f7d254bc266e38ee518bc8cce1986e68..3a4f6e775471be230f7b563107c71f8cbe89a097 100644 (file)
@@ -1107,6 +1107,16 @@ repairDependencyLoop(DumpableObject **loop,
                }
        }
 
+       /* Loop of table with itself, happens with generated columns */
+       if (nLoop == 1)
+       {
+               if (loop[0]->objType == DO_TABLE)
+               {
+                       removeObjectDependency(loop[0], loop[0]->dumpId);
+                       return;
+               }
+       }
+
        /*
         * If all the objects are TABLE_DATA items, what we must have is a
         * circular set of foreign key constraints (or a single self-referential
index de6895122e51e9dcdea9eb6f54b55de18101071a..a69375056d23e70012007a10bfb78e2401825097 100644 (file)
@@ -2392,6 +2392,23 @@ my %tests = (
                unlike => { exclude_dump_test_schema => 1, },
        },
 
+       'CREATE TABLE test_table_generated' => {
+               create_order => 3,
+               create_sql   => 'CREATE TABLE dump_test.test_table_generated (
+                                                  col1 int primary key,
+                                                  col2 int generated always as (col1 * 2) stored
+                                          );',
+               regexp => qr/^
+                       \QCREATE TABLE dump_test.test_table_generated (\E\n
+                       \s+\Qcol1 integer NOT NULL,\E\n
+                       \s+\Qcol2 integer GENERATED ALWAYS AS ((col1 * 2)) STORED\E\n
+                       \);
+                       /xms,
+               like =>
+                 { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+               unlike => { exclude_dump_test_schema => 1, },
+       },
+
        'CREATE TABLE table_with_stats' => {
                create_order => 98,
                create_sql   => 'CREATE TABLE dump_test.table_index_stats (
index 2f8a4d752ab7c4f7ef201503a083d72e9250dc4e..f94a7a9c30a01d191d860530ec1dfab732e85078 100644 (file)
@@ -1464,6 +1464,7 @@ describeOneTableDetails(const char *schemaname,
                                attnotnull_col = -1,
                                attcoll_col = -1,
                                attidentity_col = -1,
+                               attgenerated_col = -1,
                                isindexkey_col = -1,
                                indexdef_col = -1,
                                fdwopts_col = -1,
@@ -1834,8 +1835,9 @@ describeOneTableDetails(const char *schemaname,
 
        if (show_column_details)
        {
+               /* use "pretty" mode for expression to avoid excessive parentheses */
                appendPQExpBufferStr(&buf,
-                                                        ",\n  (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)"
+                                                        ",\n  (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128)"
                                                         "\n   FROM pg_catalog.pg_attrdef d"
                                                         "\n   WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef)"
                                                         ",\n  a.attnotnull");
@@ -1852,6 +1854,11 @@ describeOneTableDetails(const char *schemaname,
                else
                        appendPQExpBufferStr(&buf, ",\n  ''::pg_catalog.char AS attidentity");
                attidentity_col = cols++;
+               if (pset.sversion >= 120000)
+                       appendPQExpBufferStr(&buf, ",\n  a.attgenerated");
+               else
+                       appendPQExpBufferStr(&buf, ",\n  ''::pg_catalog.char AS attgenerated");
+               attgenerated_col = cols++;
        }
        if (tableinfo.relkind == RELKIND_INDEX ||
                tableinfo.relkind == RELKIND_PARTITIONED_INDEX)
@@ -2032,6 +2039,7 @@ describeOneTableDetails(const char *schemaname,
                if (show_column_details)
                {
                        char       *identity;
+                       char       *generated;
                        char       *default_str = "";
 
                        printTableAddCell(&cont, PQgetvalue(res, i, attcoll_col), false, false);
@@ -2041,16 +2049,19 @@ describeOneTableDetails(const char *schemaname,
                                                          false, false);
 
                        identity = PQgetvalue(res, i, attidentity_col);
+                       generated = PQgetvalue(res, i, attgenerated_col);
 
-                       if (!identity[0])
-                               /* (note: above we cut off the 'default' string at 128) */
-                               default_str = PQgetvalue(res, i, attrdef_col);
-                       else if (identity[0] == ATTRIBUTE_IDENTITY_ALWAYS)
+                       if (identity[0] == ATTRIBUTE_IDENTITY_ALWAYS)
                                default_str = "generated always as identity";
                        else if (identity[0] == ATTRIBUTE_IDENTITY_BY_DEFAULT)
                                default_str = "generated by default as identity";
+                       else if (generated[0] == ATTRIBUTE_GENERATED_STORED)
+                               default_str = psprintf("generated always as (%s) stored", PQgetvalue(res, i, attrdef_col));
+                       else
+                               /* (note: above we cut off the 'default' string at 128) */
+                               default_str = PQgetvalue(res, i, attrdef_col);
 
-                       printTableAddCell(&cont, default_str, false, false);
+                       printTableAddCell(&cont, default_str, false, generated[0] ? true : false);
                }
 
                /* Info for index columns */
index 66d1b2fc40ed3c50b7d236f770f628e3bd01bfd0..a592d22a0ee6d771c8c4b536a308c7e69c49575a 100644 (file)
@@ -42,6 +42,7 @@ typedef struct TupleConstr
        uint16          num_defval;
        uint16          num_check;
        bool            has_not_null;
+       bool            has_generated_stored;
 } TupleConstr;
 
 /*
index 485cf422d960d8602eab9c424185d15a8fca8384..6155c3234d97910ca1e5f461ad91e657a02b81a4 100644 (file)
@@ -53,6 +53,6 @@
  */
 
 /*                                                     yyyymmddN */
-#define CATALOG_VERSION_NO     201903291
+#define CATALOG_VERSION_NO     201903301
 
 #endif
index f58d74edca19d59fc83908d7a4b5e40db490c7d9..0d8e5e5b27e048afc2712a5732023c8268c39c97 100644 (file)
@@ -28,6 +28,7 @@ typedef struct RawColumnDefault
        AttrNumber      attnum;                 /* attribute to attach default to */
        Node       *raw_default;        /* default value (untransformed parse tree) */
        bool            missingMode;    /* true if part of add column processing */
+       char            generated;              /* attgenerated setting */
 } RawColumnDefault;
 
 typedef struct CookedConstraint
@@ -120,7 +121,8 @@ extern Node *cookDefault(ParseState *pstate,
                        Node *raw_default,
                        Oid atttypid,
                        int32 atttypmod,
-                       const char *attname);
+                       const char *attname,
+                       char attgenerated);
 
 extern void DeleteRelationTuple(Oid relid);
 extern void DeleteAttributeTuples(Oid relid);
index a6ec122389dac0c93111fa5a2f763ab48e621361..04004b5703ee4e8ade5301d29db633516315de3c 100644 (file)
@@ -140,6 +140,9 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
        /* One of the ATTRIBUTE_IDENTITY_* constants below, or '\0' */
        char            attidentity BKI_DEFAULT('\0');
 
+       /* One of the ATTRIBUTE_GENERATED_* constants below, or '\0' */
+       char            attgenerated BKI_DEFAULT('\0');
+
        /* Is dropped (ie, logically invisible) or not */
        bool            attisdropped BKI_DEFAULT(f);
 
@@ -201,6 +204,8 @@ typedef FormData_pg_attribute *Form_pg_attribute;
 #define                  ATTRIBUTE_IDENTITY_ALWAYS             'a'
 #define                  ATTRIBUTE_IDENTITY_BY_DEFAULT 'd'
 
+#define                  ATTRIBUTE_GENERATED_STORED    's'
+
 #endif                                                 /* EXPOSE_TO_CLIENT_CODE */
 
 #endif                                                 /* PG_ATTRIBUTE_H */
index c89710bc601e560122cae65c51a357352a78d0ec..9bcf28676da0407a2c2503b2d0a7aeb8f44c2ee3 100644 (file)
@@ -34,7 +34,7 @@
   relname => 'pg_attribute', reltype => 'pg_attribute', relam => 'heap',
   relfilenode => '0', relpages => '0', reltuples => '0', relallvisible => '0',
   reltoastrelid => '0', relhasindex => 'f', relisshared => 'f',
-  relpersistence => 'p', relkind => 'r', relnatts => '24', relchecks => '0',
+  relpersistence => 'p', relkind => 'r', relnatts => '25', relchecks => '0',
   relhasrules => 'f', relhastriggers => 'f', relhassubclass => 'f',
   relrowsecurity => 'f', relforcerowsecurity => 'f', relispopulated => 't',
   relreplident => 'n', relispartition => 'f', relfrozenxid => '3',
index b8b289efc0e0e9dab1719da3ba27e101a8c51896..891b119608b051d890f177c7aab0695cd32456aa 100644 (file)
@@ -15,6 +15,8 @@
 
 #include "nodes/execnodes.h"
 
+extern void ExecComputeStoredGenerated(EState *estate, TupleTableSlot *slot);
+
 extern ModifyTableState *ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags);
 extern void ExecEndModifyTable(ModifyTableState *node);
 extern void ExecReScanModifyTable(ModifyTableState *node);
index 869c303e157b94212848b9c80e49ab84cac6c2c3..dbd7ed0363b9286f453c9a0745b70eb59981341d 100644 (file)
@@ -452,6 +452,9 @@ typedef struct ResultRelInfo
        /* array of constraint-checking expr states */
        ExprState **ri_ConstraintExprs;
 
+       /* array of stored generated columns expr states */
+       ExprState **ri_GeneratedExprs;
+
        /* for removing junk attributes from tuples */
        JunkFilter *ri_junkFilter;
 
index e81c6269137660260a365774ce16f4d5b20f1575..94c0b7a9dd50de428a4ce660d6fa92d5dbbcadf3 100644 (file)
@@ -655,6 +655,7 @@ typedef struct ColumnDef
        char            identity;               /* attidentity setting */
        RangeVar   *identitySequence;   /* to store identity sequence name for
                                                                         * ALTER TABLE ... ADD COLUMN */
+       char            generated;              /* attgenerated setting */
        CollateClause *collClause;      /* untransformed COLLATE spec, if any */
        Oid                     collOid;                /* collation OID (InvalidOid if not set) */
        List       *constraints;        /* other constraints on column */
@@ -677,10 +678,11 @@ typedef enum TableLikeOption
        CREATE_TABLE_LIKE_COMMENTS = 1 << 0,
        CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 1,
        CREATE_TABLE_LIKE_DEFAULTS = 1 << 2,
-       CREATE_TABLE_LIKE_IDENTITY = 1 << 3,
-       CREATE_TABLE_LIKE_INDEXES = 1 << 4,
-       CREATE_TABLE_LIKE_STATISTICS = 1 << 5,
-       CREATE_TABLE_LIKE_STORAGE = 1 << 6,
+       CREATE_TABLE_LIKE_GENERATED = 1 << 3,
+       CREATE_TABLE_LIKE_IDENTITY = 1 << 4,
+       CREATE_TABLE_LIKE_INDEXES = 1 << 5,
+       CREATE_TABLE_LIKE_STATISTICS = 1 << 6,
+       CREATE_TABLE_LIKE_STORAGE = 1 << 7,
        CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -933,6 +935,15 @@ typedef struct PartitionCmd
  *       them in these fields.  A whole-row Var reference is represented by
  *       setting the bit for InvalidAttrNumber.
  *
+ *       updatedCols is also used in some other places, for example, to determine
+ *       which triggers to fire and in FDWs to know which changed columns they
+ *       need to ship off.  Generated columns that are caused to be updated by an
+ *       update to a base column are collected in extraUpdatedCols.  This is not
+ *       considered for permission checking, but it is useful in those places
+ *       that want to know the full set of columns being updated as opposed to
+ *       only the ones the user explicitly mentioned in the query.  (There is
+ *       currently no need for an extraInsertedCols, but it could exist.)
+ *
  *       securityQuals is a list of security barrier quals (boolean expressions),
  *       to be tested in the listed order before returning a row from the
  *       relation.  It is always NIL in parser output.  Entries are added by the
@@ -1087,6 +1098,7 @@ typedef struct RangeTblEntry
        Bitmapset  *selectedCols;       /* columns needing SELECT permission */
        Bitmapset  *insertedCols;       /* columns needing INSERT permission */
        Bitmapset  *updatedCols;        /* columns needing UPDATE permission */
+       Bitmapset  *extraUpdatedCols;   /* generated columns being updated */
        List       *securityQuals;      /* security barrier quals to apply, if any */
 } RangeTblEntry;
 
@@ -2086,6 +2098,7 @@ typedef enum ConstrType                   /* types of constraints */
        CONSTR_NOTNULL,
        CONSTR_DEFAULT,
        CONSTR_IDENTITY,
+       CONSTR_GENERATED,
        CONSTR_CHECK,
        CONSTR_PRIMARY,
        CONSTR_UNIQUE,
@@ -2124,7 +2137,8 @@ typedef struct Constraint
        bool            is_no_inherit;  /* is constraint non-inheritable? */
        Node       *raw_expr;           /* expr, as untransformed parse tree */
        char       *cooked_expr;        /* expr, as nodeToString representation */
-       char            generated_when;
+       char            generated_when; /* ALWAYS or BY DEFAULT */
+       char            generated_kind; /* currently always STORED */
 
        /* Fields used for unique constraints (UNIQUE and PRIMARY KEY): */
        List       *keys;                       /* String nodes naming referenced key
index c337f047cb753b2dfbc57378f46b7958aeaa3dc6..c556e0f2589486dd169ec77a3c0a7f1f425dfea3 100644 (file)
@@ -71,4 +71,6 @@ extern double get_function_rows(PlannerInfo *root, Oid funcid, Node *node);
 
 extern bool has_row_triggers(PlannerInfo *root, Index rti, CmdType event);
 
+extern bool has_stored_generated_columns(PlannerInfo *root, Index rti);
+
 #endif                                                 /* PLANCAT_H */
index f05444008c4680c2f671847a759dc0851e63a797..00ace8425e21c0e8a8c9ba306228a6b2f15b3368 100644 (file)
@@ -383,6 +383,7 @@ PG_KEYWORD("statistics", STATISTICS, UNRESERVED_KEYWORD)
 PG_KEYWORD("stdin", STDIN, UNRESERVED_KEYWORD)
 PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD)
 PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD)
+PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD)
 PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD)
 PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD)
 PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD)
index ea99a0954ba16de68d0e4c7b1ffa8203e3bc1e69..3d8039aa518ed7943960f0b759290ea6fe8d851d 100644 (file)
@@ -71,7 +71,8 @@ typedef enum ParseExprKind
        EXPR_KIND_PARTITION_BOUND,      /* partition bound expression */
        EXPR_KIND_PARTITION_EXPRESSION, /* PARTITION BY expression */
        EXPR_KIND_CALL_ARGUMENT,                /* procedure argument in CALL */
-       EXPR_KIND_COPY_WHERE            /* WHERE condition in COPY FROM */
+       EXPR_KIND_COPY_WHERE,           /* WHERE condition in COPY FROM */
+       EXPR_KIND_GENERATED_COLUMN,     /* generation expression for a column */
 } ParseExprKind;
 
 
index b9a9ecb7cc3a6c6e69d0b57767d8dc78fc9ab46a..9606d021b10b6b76a7d90807d2bb4b87dd4fa2db 100644 (file)
@@ -86,6 +86,7 @@ extern Oid get_opfamily_proc(Oid opfamily, Oid lefttype, Oid righttype,
                                  int16 procnum);
 extern char *get_attname(Oid relid, AttrNumber attnum, bool missing_ok);
 extern AttrNumber get_attnum(Oid relid, const char *attname);
+extern char get_attgenerated(Oid relid, AttrNumber attnum);
 extern Oid     get_atttype(Oid relid, AttrNumber attnum);
 extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum,
                                          Oid *typid, int32 *typmod, Oid *collid);
index 28011cd9f643cd4270ccac560176063514d57d45..d4879e2f03b04fbac2c6541b4a86e070b185c93f 100644 (file)
@@ -6,6 +6,10 @@ CREATE TABLE trigger_test (
         v varchar,
                foo rowcompnest
 );
+CREATE TABLE trigger_test_generated (
+    i int,
+    j int GENERATED ALWAYS AS (i * 2) STORED
+);
 CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger LANGUAGE plperl AS $$
 
   # make sure keys are sorted for consistent results - perl no longer
@@ -98,6 +102,79 @@ NOTICE:  $_TD->{table_name} = 'trigger_test'
 NOTICE:  $_TD->{table_schema} = 'public'
 NOTICE:  $_TD->{when} = 'BEFORE'
 DROP TRIGGER show_trigger_data_trig on trigger_test;
+CREATE TRIGGER show_trigger_data_trig_before
+BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+CREATE TRIGGER show_trigger_data_trig_after
+AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+insert into trigger_test_generated (i) values (1);
+NOTICE:  $_TD->{argc} = '0'
+NOTICE:  $_TD->{event} = 'INSERT'
+NOTICE:  $_TD->{level} = 'ROW'
+NOTICE:  $_TD->{name} = 'show_trigger_data_trig_before'
+NOTICE:  $_TD->{new} = {'i' => '1'}
+NOTICE:  $_TD->{relid} = 'bogus:12345'
+NOTICE:  $_TD->{relname} = 'trigger_test_generated'
+NOTICE:  $_TD->{table_name} = 'trigger_test_generated'
+NOTICE:  $_TD->{table_schema} = 'public'
+NOTICE:  $_TD->{when} = 'BEFORE'
+NOTICE:  $_TD->{argc} = '0'
+NOTICE:  $_TD->{event} = 'INSERT'
+NOTICE:  $_TD->{level} = 'ROW'
+NOTICE:  $_TD->{name} = 'show_trigger_data_trig_after'
+NOTICE:  $_TD->{new} = {'i' => '1', 'j' => '2'}
+NOTICE:  $_TD->{relid} = 'bogus:12345'
+NOTICE:  $_TD->{relname} = 'trigger_test_generated'
+NOTICE:  $_TD->{table_name} = 'trigger_test_generated'
+NOTICE:  $_TD->{table_schema} = 'public'
+NOTICE:  $_TD->{when} = 'AFTER'
+update trigger_test_generated set i = 11 where i = 1;
+NOTICE:  $_TD->{argc} = '0'
+NOTICE:  $_TD->{event} = 'UPDATE'
+NOTICE:  $_TD->{level} = 'ROW'
+NOTICE:  $_TD->{name} = 'show_trigger_data_trig_before'
+NOTICE:  $_TD->{new} = {'i' => '11'}
+NOTICE:  $_TD->{old} = {'i' => '1', 'j' => '2'}
+NOTICE:  $_TD->{relid} = 'bogus:12345'
+NOTICE:  $_TD->{relname} = 'trigger_test_generated'
+NOTICE:  $_TD->{table_name} = 'trigger_test_generated'
+NOTICE:  $_TD->{table_schema} = 'public'
+NOTICE:  $_TD->{when} = 'BEFORE'
+NOTICE:  $_TD->{argc} = '0'
+NOTICE:  $_TD->{event} = 'UPDATE'
+NOTICE:  $_TD->{level} = 'ROW'
+NOTICE:  $_TD->{name} = 'show_trigger_data_trig_after'
+NOTICE:  $_TD->{new} = {'i' => '11', 'j' => '22'}
+NOTICE:  $_TD->{old} = {'i' => '1', 'j' => '2'}
+NOTICE:  $_TD->{relid} = 'bogus:12345'
+NOTICE:  $_TD->{relname} = 'trigger_test_generated'
+NOTICE:  $_TD->{table_name} = 'trigger_test_generated'
+NOTICE:  $_TD->{table_schema} = 'public'
+NOTICE:  $_TD->{when} = 'AFTER'
+delete from trigger_test_generated;
+NOTICE:  $_TD->{argc} = '0'
+NOTICE:  $_TD->{event} = 'DELETE'
+NOTICE:  $_TD->{level} = 'ROW'
+NOTICE:  $_TD->{name} = 'show_trigger_data_trig_before'
+NOTICE:  $_TD->{old} = {'i' => '11', 'j' => '22'}
+NOTICE:  $_TD->{relid} = 'bogus:12345'
+NOTICE:  $_TD->{relname} = 'trigger_test_generated'
+NOTICE:  $_TD->{table_name} = 'trigger_test_generated'
+NOTICE:  $_TD->{table_schema} = 'public'
+NOTICE:  $_TD->{when} = 'BEFORE'
+NOTICE:  $_TD->{argc} = '0'
+NOTICE:  $_TD->{event} = 'DELETE'
+NOTICE:  $_TD->{level} = 'ROW'
+NOTICE:  $_TD->{name} = 'show_trigger_data_trig_after'
+NOTICE:  $_TD->{old} = {'i' => '11', 'j' => '22'}
+NOTICE:  $_TD->{relid} = 'bogus:12345'
+NOTICE:  $_TD->{relname} = 'trigger_test_generated'
+NOTICE:  $_TD->{table_name} = 'trigger_test_generated'
+NOTICE:  $_TD->{table_schema} = 'public'
+NOTICE:  $_TD->{when} = 'AFTER'
+DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
+DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;
 insert into trigger_test values(1,'insert', '("(1)")');
 CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
 CREATE TRIGGER show_trigger_data_trig
@@ -295,3 +372,21 @@ NOTICE:  perlsnitch: ddl_command_start DROP TABLE
 NOTICE:  perlsnitch: ddl_command_end DROP TABLE 
 drop event trigger perl_a_snitch;
 drop event trigger perl_b_snitch;
+-- dealing with generated columns
+CREATE FUNCTION generated_test_func1() RETURNS trigger
+LANGUAGE plperl
+AS $$
+$_TD->{new}{j} = 5;  # not allowed
+return 'MODIFY';
+$$;
+CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
+TRUNCATE trigger_test_generated;
+INSERT INTO trigger_test_generated (i) VALUES (1);
+ERROR:  cannot set generated column "j"
+CONTEXT:  PL/Perl function "generated_test_func1"
+SELECT * FROM trigger_test_generated;
+ i | j 
+---+---
+(0 rows)
+
index 35d5d121a08423de59b4e8cb0c2ff86737ebac88..31ba2f262f7ed0820919d7a9802a99df09c1ee35 100644 (file)
@@ -266,7 +266,7 @@ static plperl_proc_desc *compile_plperl_function(Oid fn_oid,
                                                bool is_trigger,
                                                bool is_event_trigger);
 
-static SV  *plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc);
+static SV  *plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc, bool include_generated);
 static SV  *plperl_hash_from_datum(Datum attr);
 static SV  *plperl_ref_from_pg_array(Datum arg, Oid typid);
 static SV  *split_array(plperl_array_info *info, int first, int last, int nest);
@@ -1644,13 +1644,19 @@ plperl_trigger_build_args(FunctionCallInfo fcinfo)
        hv_store_string(hv, "name", cstr2sv(tdata->tg_trigger->tgname));
        hv_store_string(hv, "relid", cstr2sv(relid));
 
+       /*
+        * Note: In BEFORE trigger, stored generated columns are not computed yet,
+        * so don't make them accessible in NEW row.
+        */
+
        if (TRIGGER_FIRED_BY_INSERT(tdata->tg_event))
        {
                event = "INSERT";
                if (TRIGGER_FIRED_FOR_ROW(tdata->tg_event))
                        hv_store_string(hv, "new",
                                                        plperl_hash_from_tuple(tdata->tg_trigtuple,
-                                                                                                  tupdesc));
+                                                                                                  tupdesc,
+                                                                                                  !TRIGGER_FIRED_BEFORE(tdata->tg_event)));
        }
        else if (TRIGGER_FIRED_BY_DELETE(tdata->tg_event))
        {
@@ -1658,7 +1664,8 @@ plperl_trigger_build_args(FunctionCallInfo fcinfo)
                if (TRIGGER_FIRED_FOR_ROW(tdata->tg_event))
                        hv_store_string(hv, "old",
                                                        plperl_hash_from_tuple(tdata->tg_trigtuple,
-                                                                                                  tupdesc));
+                                                                                                  tupdesc,
+                                                                                                  true));
        }
        else if (TRIGGER_FIRED_BY_UPDATE(tdata->tg_event))
        {
@@ -1667,10 +1674,12 @@ plperl_trigger_build_args(FunctionCallInfo fcinfo)
                {
                        hv_store_string(hv, "old",
                                                        plperl_hash_from_tuple(tdata->tg_trigtuple,
-                                                                                                  tupdesc));
+                                                                                                  tupdesc,
+                                                                                                  true));
                        hv_store_string(hv, "new",
                                                        plperl_hash_from_tuple(tdata->tg_newtuple,
-                                                                                                  tupdesc));
+                                                                                                  tupdesc,
+                                                                                                  !TRIGGER_FIRED_BEFORE(tdata->tg_event)));
                }
        }
        else if (TRIGGER_FIRED_BY_TRUNCATE(tdata->tg_event))
@@ -1791,6 +1800,11 @@ plperl_modify_tuple(HV *hvTD, TriggerData *tdata, HeapTuple otup)
                                        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                                         errmsg("cannot set system attribute \"%s\"",
                                                        key)));
+               if (attr->attgenerated)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+                                        errmsg("cannot set generated column \"%s\"",
+                                                       key)));
 
                modvalues[attn - 1] = plperl_sv_to_datum(val,
                                                                                                 attr->atttypid,
@@ -3012,7 +3026,7 @@ plperl_hash_from_datum(Datum attr)
        tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
        tmptup.t_data = td;
 
-       sv = plperl_hash_from_tuple(&tmptup, tupdesc);
+       sv = plperl_hash_from_tuple(&tmptup, tupdesc, true);
        ReleaseTupleDesc(tupdesc);
 
        return sv;
@@ -3020,7 +3034,7 @@ plperl_hash_from_datum(Datum attr)
 
 /* Build a hash from all attributes of a given tuple. */
 static SV  *
-plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc)
+plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc, bool include_generated)
 {
        dTHX;
        HV                 *hv;
@@ -3044,6 +3058,13 @@ plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc)
                if (att->attisdropped)
                        continue;
 
+               if (att->attgenerated)
+               {
+                       /* don't include unless requested */
+                       if (!include_generated)
+                               continue;
+               }
+
                attname = NameStr(att->attname);
                attr = heap_getattr(tuple, i + 1, tupdesc, &isnull);
 
@@ -3198,7 +3219,7 @@ plperl_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 processed,
                av_extend(rows, processed);
                for (i = 0; i < processed; i++)
                {
-                       row = plperl_hash_from_tuple(tuptable->vals[i], tuptable->tupdesc);
+                       row = plperl_hash_from_tuple(tuptable->vals[i], tuptable->tupdesc, true);
                        av_push(rows, row);
                }
                hv_store_string(result, "rows",
@@ -3484,7 +3505,8 @@ plperl_spi_fetchrow(char *cursor)
                        else
                        {
                                row = plperl_hash_from_tuple(SPI_tuptable->vals[0],
-                                                                                        SPI_tuptable->tupdesc);
+                                                                                        SPI_tuptable->tupdesc,
+                                                                                        true);
                        }
                        SPI_freetuptable(SPI_tuptable);
                }
index 624193b9d08e69262cb89bb4ff2576bffdde8478..4adddeb80ac5802f8f414c583d33bf7b6e7df2df 100644 (file)
@@ -8,6 +8,11 @@ CREATE TABLE trigger_test (
                foo rowcompnest
 );
 
+CREATE TABLE trigger_test_generated (
+    i int,
+    j int GENERATED ALWAYS AS (i * 2) STORED
+);
+
 CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger LANGUAGE plperl AS $$
 
   # make sure keys are sorted for consistent results - perl no longer
@@ -70,6 +75,21 @@ delete from trigger_test;
 
 DROP TRIGGER show_trigger_data_trig on trigger_test;
 
+CREATE TRIGGER show_trigger_data_trig_before
+BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+
+CREATE TRIGGER show_trigger_data_trig_after
+AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+
+insert into trigger_test_generated (i) values (1);
+update trigger_test_generated set i = 11 where i = 1;
+delete from trigger_test_generated;
+
+DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
+DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;
+
 insert into trigger_test values(1,'insert', '("(1)")');
 CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
 
@@ -221,3 +241,19 @@ drop table foo;
 
 drop event trigger perl_a_snitch;
 drop event trigger perl_b_snitch;
+
+-- dealing with generated columns
+
+CREATE FUNCTION generated_test_func1() RETURNS trigger
+LANGUAGE plperl
+AS $$
+$_TD->{new}{j} = 5;  # not allowed
+return 'MODIFY';
+$$;
+
+CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
+
+TRUNCATE trigger_test_generated;
+INSERT INTO trigger_test_generated (i) VALUES (1);
+SELECT * FROM trigger_test_generated;
index 527cada4feba0feaefd4b278a142b0b86e05ac27..f0005009b2c31887751a8c07104ad47d097378ba 100644 (file)
@@ -924,6 +924,26 @@ plpgsql_exec_trigger(PLpgSQL_function *func,
                                                                  false, false);
                expanded_record_set_tuple(rec_old->erh, trigdata->tg_trigtuple,
                                                                  false, false);
+
+               /*
+                * In BEFORE trigger, stored generated columns are not computed yet,
+                * so make them null in the NEW row.  (Only needed in UPDATE branch;
+                * in the INSERT case, they are already null, but in UPDATE, the field
+                * still contains the old value.)  Alternatively, we could construct a
+                * whole new row structure without the generated columns, but this way
+                * seems more efficient and potentially less confusing.
+                */
+               if (tupdesc->constr && tupdesc->constr->has_generated_stored &&
+                       TRIGGER_FIRED_BEFORE(trigdata->tg_event))
+               {
+                       for (int i = 0; i < tupdesc->natts; i++)
+                               if (TupleDescAttr(tupdesc, i)->attgenerated == ATTRIBUTE_GENERATED_STORED)
+                                       expanded_record_set_field_internal(rec_new->erh,
+                                                                                                          i + 1,
+                                                                                                          (Datum) 0,
+                                                                                                          true, /*isnull*/
+                                                                                                          false, false);
+               }
        }
        else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
        {
index d7ab8ac6b8e20b3e155336d746eb9c7adb99fc65..742988a5b59eb0b19388035ecc4a98acab7596b3 100644 (file)
@@ -67,6 +67,10 @@ SELECT * FROM users;
 -- dump trigger data
 CREATE TABLE trigger_test
        (i int, v text );
+CREATE TABLE trigger_test_generated (
+       i int,
+        j int GENERATED ALWAYS AS (i * 2) STORED
+);
 CREATE FUNCTION trigger_data() RETURNS trigger LANGUAGE plpythonu AS $$
 
 if 'relid' in TD:
@@ -203,6 +207,77 @@ NOTICE:  TD[when] => BEFORE
 DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
 DROP TRIGGER show_trigger_data_trig_before on trigger_test;
 DROP TRIGGER show_trigger_data_trig_after on trigger_test;
+CREATE TRIGGER show_trigger_data_trig_before
+BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+CREATE TRIGGER show_trigger_data_trig_after
+AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+insert into trigger_test_generated (i) values (1);
+NOTICE:  TD[args] => None
+NOTICE:  TD[event] => INSERT
+NOTICE:  TD[level] => ROW
+NOTICE:  TD[name] => show_trigger_data_trig_before
+NOTICE:  TD[new] => {'i': 1}
+NOTICE:  TD[old] => None
+NOTICE:  TD[relid] => bogus:12345
+NOTICE:  TD[table_name] => trigger_test_generated
+NOTICE:  TD[table_schema] => public
+NOTICE:  TD[when] => BEFORE
+NOTICE:  TD[args] => None
+NOTICE:  TD[event] => INSERT
+NOTICE:  TD[level] => ROW
+NOTICE:  TD[name] => show_trigger_data_trig_after
+NOTICE:  TD[new] => {'i': 1, 'j': 2}
+NOTICE:  TD[old] => None
+NOTICE:  TD[relid] => bogus:12345
+NOTICE:  TD[table_name] => trigger_test_generated
+NOTICE:  TD[table_schema] => public
+NOTICE:  TD[when] => AFTER
+update trigger_test_generated set i = 11 where i = 1;
+NOTICE:  TD[args] => None
+NOTICE:  TD[event] => UPDATE
+NOTICE:  TD[level] => ROW
+NOTICE:  TD[name] => show_trigger_data_trig_before
+NOTICE:  TD[new] => {'i': 11}
+NOTICE:  TD[old] => {'i': 1, 'j': 2}
+NOTICE:  TD[relid] => bogus:12345
+NOTICE:  TD[table_name] => trigger_test_generated
+NOTICE:  TD[table_schema] => public
+NOTICE:  TD[when] => BEFORE
+NOTICE:  TD[args] => None
+NOTICE:  TD[event] => UPDATE
+NOTICE:  TD[level] => ROW
+NOTICE:  TD[name] => show_trigger_data_trig_after
+NOTICE:  TD[new] => {'i': 11, 'j': 22}
+NOTICE:  TD[old] => {'i': 1, 'j': 2}
+NOTICE:  TD[relid] => bogus:12345
+NOTICE:  TD[table_name] => trigger_test_generated
+NOTICE:  TD[table_schema] => public
+NOTICE:  TD[when] => AFTER
+delete from trigger_test_generated;
+NOTICE:  TD[args] => None
+NOTICE:  TD[event] => DELETE
+NOTICE:  TD[level] => ROW
+NOTICE:  TD[name] => show_trigger_data_trig_before
+NOTICE:  TD[new] => None
+NOTICE:  TD[old] => {'i': 11, 'j': 22}
+NOTICE:  TD[relid] => bogus:12345
+NOTICE:  TD[table_name] => trigger_test_generated
+NOTICE:  TD[table_schema] => public
+NOTICE:  TD[when] => BEFORE
+NOTICE:  TD[args] => None
+NOTICE:  TD[event] => DELETE
+NOTICE:  TD[level] => ROW
+NOTICE:  TD[name] => show_trigger_data_trig_after
+NOTICE:  TD[new] => None
+NOTICE:  TD[old] => {'i': 11, 'j': 22}
+NOTICE:  TD[relid] => bogus:12345
+NOTICE:  TD[table_name] => trigger_test_generated
+NOTICE:  TD[table_schema] => public
+NOTICE:  TD[when] => AFTER
+DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
+DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;
 insert into trigger_test values(1,'insert');
 CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
 CREATE TRIGGER show_trigger_data_trig
@@ -524,3 +599,22 @@ INFO:  old: 1 -> a
 INFO:  new: 1 -> b
 DROP TABLE transition_table_test;
 DROP FUNCTION transition_table_test_f();
+-- dealing with generated columns
+CREATE FUNCTION generated_test_func1() RETURNS trigger
+LANGUAGE plpythonu
+AS $$
+TD['new']['j'] = 5  # not allowed
+return 'MODIFY'
+$$;
+CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
+TRUNCATE trigger_test_generated;
+INSERT INTO trigger_test_generated (i) VALUES (1);
+ERROR:  cannot set generated column "j"
+CONTEXT:  while modifying trigger row
+PL/Python function "generated_test_func1"
+SELECT * FROM trigger_test_generated;
+ i | j 
+---+---
+(0 rows)
+
index 45ac25b2ae0a0234afdaffcf6573471c6855c98f..e4d543a4d4614d9a6432acd374f6c29c82870e01 100644 (file)
@@ -357,7 +357,7 @@ PLy_cursor_iternext(PyObject *self)
                                                                  exec_ctx->curr_proc);
 
                        ret = PLy_input_from_tuple(&cursor->result, SPI_tuptable->vals[0],
-                                                                          SPI_tuptable->tupdesc);
+                                                                          SPI_tuptable->tupdesc, true);
                }
 
                SPI_freetuptable(SPI_tuptable);
@@ -453,7 +453,8 @@ PLy_cursor_fetch(PyObject *self, PyObject *args)
                                {
                                        PyObject   *row = PLy_input_from_tuple(&cursor->result,
                                                                                                                   SPI_tuptable->vals[i],
-                                                                                                                  SPI_tuptable->tupdesc);
+                                                                                                                  SPI_tuptable->tupdesc,
+                                                                                                                  true);
 
                                        PyList_SetItem(ret->rows, i, row);
                                }
index 213718624183ee42360c7c940b2184c7e57392be..fd6cdc4ce55e971905fe37f7398184ac29a8cd52 100644 (file)
@@ -13,6 +13,7 @@
 #include "executor/spi.h"
 #include "funcapi.h"
 #include "utils/builtins.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 #include "utils/typcache.h"
 
@@ -751,6 +752,11 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
                        PyDict_SetItemString(pltdata, "level", pltlevel);
                        Py_DECREF(pltlevel);
 
+                       /*
+                        * Note: In BEFORE trigger, stored generated columns are not computed yet,
+                        * so don't make them accessible in NEW row.
+                        */
+
                        if (TRIGGER_FIRED_BY_INSERT(tdata->tg_event))
                        {
                                pltevent = PyString_FromString("INSERT");
@@ -758,7 +764,8 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
                                PyDict_SetItemString(pltdata, "old", Py_None);
                                pytnew = PLy_input_from_tuple(&proc->result_in,
                                                                                          tdata->tg_trigtuple,
-                                                                                         rel_descr);
+                                                                                         rel_descr,
+                                                                                         !TRIGGER_FIRED_BEFORE(tdata->tg_event));
                                PyDict_SetItemString(pltdata, "new", pytnew);
                                Py_DECREF(pytnew);
                                *rv = tdata->tg_trigtuple;
@@ -770,7 +777,8 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
                                PyDict_SetItemString(pltdata, "new", Py_None);
                                pytold = PLy_input_from_tuple(&proc->result_in,
                                                                                          tdata->tg_trigtuple,
-                                                                                         rel_descr);
+                                                                                         rel_descr,
+                                                                                         true);
                                PyDict_SetItemString(pltdata, "old", pytold);
                                Py_DECREF(pytold);
                                *rv = tdata->tg_trigtuple;
@@ -781,12 +789,14 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
 
                                pytnew = PLy_input_from_tuple(&proc->result_in,
                                                                                          tdata->tg_newtuple,
-                                                                                         rel_descr);
+                                                                                         rel_descr,
+                                                                                         !TRIGGER_FIRED_BEFORE(tdata->tg_event));
                                PyDict_SetItemString(pltdata, "new", pytnew);
                                Py_DECREF(pytnew);
                                pytold = PLy_input_from_tuple(&proc->result_in,
                                                                                          tdata->tg_trigtuple,
-                                                                                         rel_descr);
+                                                                                         rel_descr,
+                                                                                         true);
                                PyDict_SetItemString(pltdata, "old", pytold);
                                Py_DECREF(pytold);
                                *rv = tdata->tg_newtuple;
@@ -952,6 +962,11 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata,
                                                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                                                 errmsg("cannot set system attribute \"%s\"",
                                                                plattstr)));
+                       if (TupleDescAttr(tupdesc, attn - 1)->attgenerated)
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+                                                errmsg("cannot set generated column \"%s\"",
+                                                               plattstr)));
 
                        plval = PyDict_GetItem(plntup, platt);
                        if (plval == NULL)
index 41155fc81ec7a10390716197de1ba0fe39b954c0..fb23a7b3a45d348a12bb51f7423be0b83c81bde5 100644 (file)
@@ -419,7 +419,8 @@ PLy_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 rows, int status)
                                        {
                                                PyObject   *row = PLy_input_from_tuple(&ininfo,
                                                                                                                           tuptable->vals[i],
-                                                                                                                          tuptable->tupdesc);
+                                                                                                                          tuptable->tupdesc,
+                                                                                                                          true);
 
                                                PyList_SetItem(result->rows, i, row);
                                        }
index d6a6a849c32746d75b23065e060fa711aee9bbc7..6365e461e921208cb8b637d7b98b300af9ba492e 100644 (file)
@@ -41,7 +41,7 @@ static PyObject *PLyList_FromArray(PLyDatumToOb *arg, Datum d);
 static PyObject *PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
                                                  char **dataptr_p, bits8 **bitmap_p, int *bitmask_p);
 static PyObject *PLyDict_FromComposite(PLyDatumToOb *arg, Datum d);
-static PyObject *PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc);
+static PyObject *PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool include_generated);
 
 /* conversion from Python objects to Datums */
 static Datum PLyObject_ToBool(PLyObToDatum *arg, PyObject *plrv,
@@ -134,7 +134,7 @@ PLy_output_convert(PLyObToDatum *arg, PyObject *val, bool *isnull)
  * but in practice all callers have the right tupdesc available.
  */
 PyObject *
-PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc)
+PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool include_generated)
 {
        PyObject   *dict;
        PLyExecutionContext *exec_ctx = PLy_current_execution_context();
@@ -148,7 +148,7 @@ PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc)
 
        oldcontext = MemoryContextSwitchTo(scratch_context);
 
-       dict = PLyDict_FromTuple(arg, tuple, desc);
+       dict = PLyDict_FromTuple(arg, tuple, desc, include_generated);
 
        MemoryContextSwitchTo(oldcontext);
 
@@ -804,7 +804,7 @@ PLyDict_FromComposite(PLyDatumToOb *arg, Datum d)
        tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
        tmptup.t_data = td;
 
-       dict = PLyDict_FromTuple(arg, &tmptup, tupdesc);
+       dict = PLyDict_FromTuple(arg, &tmptup, tupdesc, true);
 
        ReleaseTupleDesc(tupdesc);
 
@@ -815,7 +815,7 @@ PLyDict_FromComposite(PLyDatumToOb *arg, Datum d)
  * Transform a tuple into a Python dict object.
  */
 static PyObject *
-PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc)
+PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool include_generated)
 {
        PyObject   *volatile dict;
 
@@ -842,6 +842,13 @@ PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc)
                        if (attr->attisdropped)
                                continue;
 
+                       if (attr->attgenerated)
+                       {
+                               /* don't include unless requested */
+                               if (!include_generated)
+                                       continue;
+                       }
+
                        key = NameStr(attr->attname);
                        vattr = heap_getattr(tuple, (i + 1), desc, &is_null);
 
index 82bdfae548792d6488bb07810ae4b6587b8fb101..f210178238f8e630a9f9475590583440b560621a 100644 (file)
@@ -151,7 +151,7 @@ extern Datum PLy_output_convert(PLyObToDatum *arg, PyObject *val,
                                   bool *isnull);
 
 extern PyObject *PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple,
-                                        TupleDesc desc);
+                                        TupleDesc desc, bool include_generated);
 
 extern void PLy_input_setup_func(PLyDatumToOb *arg, MemoryContext arg_mcxt,
                                         Oid typeOid, int32 typmod,
index 79c24b714b500078beb0d456c485dc61d8fbaa53..19852dc58510f2974be0106eef7329a564167cc1 100644 (file)
@@ -67,6 +67,11 @@ SELECT * FROM users;
 CREATE TABLE trigger_test
        (i int, v text );
 
+CREATE TABLE trigger_test_generated (
+       i int,
+        j int GENERATED ALWAYS AS (i * 2) STORED
+);
+
 CREATE FUNCTION trigger_data() RETURNS trigger LANGUAGE plpythonu AS $$
 
 if 'relid' in TD:
@@ -109,6 +114,21 @@ DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
 DROP TRIGGER show_trigger_data_trig_before on trigger_test;
 DROP TRIGGER show_trigger_data_trig_after on trigger_test;
 
+CREATE TRIGGER show_trigger_data_trig_before
+BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+
+CREATE TRIGGER show_trigger_data_trig_after
+AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+
+insert into trigger_test_generated (i) values (1);
+update trigger_test_generated set i = 11 where i = 1;
+delete from trigger_test_generated;
+
+DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
+DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;
+
 insert into trigger_test values(1,'insert');
 CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
 
@@ -430,3 +450,20 @@ UPDATE transition_table_test SET name = 'b';
 
 DROP TABLE transition_table_test;
 DROP FUNCTION transition_table_test_f();
+
+
+-- dealing with generated columns
+
+CREATE FUNCTION generated_test_func1() RETURNS trigger
+LANGUAGE plpythonu
+AS $$
+TD['new']['j'] = 5  # not allowed
+return 'MODIFY'
+$$;
+
+CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
+
+TRUNCATE trigger_test_generated;
+INSERT INTO trigger_test_generated (i) VALUES (1);
+SELECT * FROM trigger_test_generated;
index 2d5daedc11591c52373faca81cb9c9bda185e4f3..008ea1950953ffc4355b5f5f1666d54e45a2f9f4 100644 (file)
@@ -61,6 +61,10 @@ CREATE TABLE trigger_test (
 );
 -- Make certain dropped attributes are handled correctly
 ALTER TABLE trigger_test DROP dropme;
+CREATE TABLE trigger_test_generated (
+   i int,
+   j int GENERATED ALWAYS AS (i * 2) STORED
+);
 CREATE VIEW trigger_test_view AS SELECT i, v FROM trigger_test;
 CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
        if {$TG_table_name eq "trigger_test" && $TG_level eq "ROW" && $TG_op ne "DELETE"} {
@@ -112,6 +116,12 @@ FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
 CREATE TRIGGER statement_trigger
 BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON trigger_test
 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_data(42,'statement trigger');
+CREATE TRIGGER show_trigger_data_trig_before
+BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+CREATE TRIGGER show_trigger_data_trig_after
+AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
 CREATE TRIGGER show_trigger_data_view_trig
 INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
 FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
@@ -631,6 +641,75 @@ NOTICE:  TG_table_name: trigger_test
 NOTICE:  TG_table_schema: public
 NOTICE:  TG_when: BEFORE
 NOTICE:  args: {23 skidoo}
+insert into trigger_test_generated (i) values (1);
+NOTICE:  NEW: {i: 1}
+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_relid: bogus:12345
+NOTICE:  TG_table_name: trigger_test_generated
+NOTICE:  TG_table_schema: public
+NOTICE:  TG_when: BEFORE
+NOTICE:  args: {}
+NOTICE:  NEW: {i: 1, j: 2}
+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_relid: bogus:12345
+NOTICE:  TG_table_name: trigger_test_generated
+NOTICE:  TG_table_schema: public
+NOTICE:  TG_when: AFTER
+NOTICE:  args: {}
+update trigger_test_generated set i = 11 where i = 1;
+NOTICE:  NEW: {i: 11}
+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_relid: bogus:12345
+NOTICE:  TG_table_name: trigger_test_generated
+NOTICE:  TG_table_schema: public
+NOTICE:  TG_when: BEFORE
+NOTICE:  args: {}
+NOTICE:  NEW: {i: 11, j: 22}
+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_relid: bogus:12345
+NOTICE:  TG_table_name: trigger_test_generated
+NOTICE:  TG_table_schema: public
+NOTICE:  TG_when: AFTER
+NOTICE:  args: {}
+delete from trigger_test_generated;
+NOTICE:  NEW: {}
+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_relid: bogus:12345
+NOTICE:  TG_table_name: trigger_test_generated
+NOTICE:  TG_table_schema: public
+NOTICE:  TG_when: BEFORE
+NOTICE:  args: {}
+NOTICE:  NEW: {}
+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_relid: bogus:12345
+NOTICE:  TG_table_name: trigger_test_generated
+NOTICE:  TG_table_schema: public
+NOTICE:  TG_when: AFTER
+NOTICE:  args: {}
 insert into trigger_test_view values(2,'insert');
 NOTICE:  NEW: {i: 2, v: insert}
 NOTICE:  OLD: {}
@@ -738,6 +817,8 @@ NOTICE:  TG_table_name: trigger_test
 NOTICE:  TG_table_schema: public
 NOTICE:  TG_when: BEFORE
 NOTICE:  args: {42 {statement trigger}}
+DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
+DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;
 -- should error
 insert into trigger_test(test_argisnull) values(true);
 NOTICE:  NEW: {}
@@ -787,3 +868,21 @@ INFO:  old: 1 -> a
 INFO:  new: 1 -> b
 drop table transition_table_test;
 drop function transition_table_test_f();
+-- dealing with generated columns
+CREATE FUNCTION generated_test_func1() RETURNS trigger
+LANGUAGE pltcl
+AS $$
+# not allowed
+set NEW(j) 5
+return [array get NEW]
+$$;
+CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
+TRUNCATE trigger_test_generated;
+INSERT INTO trigger_test_generated (i) VALUES (1);
+ERROR:  cannot set generated column "j"
+SELECT * FROM trigger_test_generated;
+ i | j 
+---+---
+(0 rows)
+
index 76c9afc33912b4793006426c38781b810ae70a18..1362ca51d1446b44e4f931bd7dbea7287ec03281 100644 (file)
@@ -324,7 +324,7 @@ static void pltcl_subtrans_abort(Tcl_Interp *interp,
 
 static void pltcl_set_tuple_values(Tcl_Interp *interp, const char *arrayname,
                                           uint64 tupno, HeapTuple tuple, TupleDesc tupdesc);
-static Tcl_Obj *pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc);
+static Tcl_Obj *pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc, bool include_generated);
 static HeapTuple pltcl_build_tuple_result(Tcl_Interp *interp,
                                                 Tcl_Obj **kvObjv, int kvObjc,
                                                 pltcl_call_state *call_state);
@@ -889,7 +889,7 @@ pltcl_func_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
                                        tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
                                        tmptup.t_data = td;
 
-                                       list_tmp = pltcl_build_tuple_argument(&tmptup, tupdesc);
+                                       list_tmp = pltcl_build_tuple_argument(&tmptup, tupdesc, true);
                                        Tcl_ListObjAppendElement(NULL, tcl_cmd, list_tmp);
 
                                        ReleaseTupleDesc(tupdesc);
@@ -1060,7 +1060,6 @@ pltcl_trigger_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
        volatile HeapTuple rettup;
        Tcl_Obj    *tcl_cmd;
        Tcl_Obj    *tcl_trigtup;
-       Tcl_Obj    *tcl_newtup;
        int                     tcl_rc;
        int                     i;
        const char *result;
@@ -1162,20 +1161,22 @@ pltcl_trigger_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
                        Tcl_ListObjAppendElement(NULL, tcl_cmd,
                                                                         Tcl_NewStringObj("ROW", -1));
 
-                       /* Build the data list for the trigtuple */
-                       tcl_trigtup = pltcl_build_tuple_argument(trigdata->tg_trigtuple,
-                                                                                                        tupdesc);
-
                        /*
                         * Now the command part of the event for TG_op and data for NEW
                         * and OLD
+                        *
+                        * Note: In BEFORE trigger, stored generated columns are not computed yet,
+                        * so don't make them accessible in NEW row.
                         */
                        if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
                        {
                                Tcl_ListObjAppendElement(NULL, tcl_cmd,
                                                                                 Tcl_NewStringObj("INSERT", -1));
 
-                               Tcl_ListObjAppendElement(NULL, tcl_cmd, tcl_trigtup);
+                               Tcl_ListObjAppendElement(NULL, tcl_cmd,
+                                                                                pltcl_build_tuple_argument(trigdata->tg_trigtuple,
+                                                                                                                                       tupdesc,
+                                                                                                                                       !TRIGGER_FIRED_BEFORE(trigdata->tg_event)));
                                Tcl_ListObjAppendElement(NULL, tcl_cmd, Tcl_NewObj());
 
                                rettup = trigdata->tg_trigtuple;
@@ -1186,7 +1187,10 @@ pltcl_trigger_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
                                                                                 Tcl_NewStringObj("DELETE", -1));
 
                                Tcl_ListObjAppendElement(NULL, tcl_cmd, Tcl_NewObj());
-                               Tcl_ListObjAppendElement(NULL, tcl_cmd, tcl_trigtup);
+                               Tcl_ListObjAppendElement(NULL, tcl_cmd,
+                                                                                pltcl_build_tuple_argument(trigdata->tg_trigtuple,
+                                                                                                                                       tupdesc,
+                                                                                                                                       true));
 
                                rettup = trigdata->tg_trigtuple;
                        }
@@ -1195,11 +1199,14 @@ pltcl_trigger_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
                                Tcl_ListObjAppendElement(NULL, tcl_cmd,
                                                                                 Tcl_NewStringObj("UPDATE", -1));
 
-                               tcl_newtup = pltcl_build_tuple_argument(trigdata->tg_newtuple,
-                                                                                                               tupdesc);
-
-                               Tcl_ListObjAppendElement(NULL, tcl_cmd, tcl_newtup);
-                               Tcl_ListObjAppendElement(NULL, tcl_cmd, tcl_trigtup);
+                               Tcl_ListObjAppendElement(NULL, tcl_cmd,
+                                                                                pltcl_build_tuple_argument(trigdata->tg_newtuple,
+                                                                                                                                       tupdesc,
+                                                                                                                                       !TRIGGER_FIRED_BEFORE(trigdata->tg_event)));
+                               Tcl_ListObjAppendElement(NULL, tcl_cmd,
+                                                                                pltcl_build_tuple_argument(trigdata->tg_trigtuple,
+                                                                                                                                       tupdesc,
+                                                                                                                                       true));
 
                                rettup = trigdata->tg_newtuple;
                        }
@@ -3091,7 +3098,7 @@ pltcl_set_tuple_values(Tcl_Interp *interp, const char *arrayname,
  *                               from all attributes of a given tuple
  **********************************************************************/
 static Tcl_Obj *
-pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc)
+pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc, bool include_generated)
 {
        Tcl_Obj    *retobj = Tcl_NewObj();
        int                     i;
@@ -3110,6 +3117,13 @@ pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc)
                if (att->attisdropped)
                        continue;
 
+               if (att->attgenerated)
+               {
+                       /* don't include unless requested */
+                       if (!include_generated)
+                               continue;
+               }
+
                /************************************************************
                 * Get the attribute name
                 ************************************************************/
@@ -3219,6 +3233,12 @@ pltcl_build_tuple_result(Tcl_Interp *interp, Tcl_Obj **kvObjv, int kvObjc,
                                         errmsg("cannot set system attribute \"%s\"",
                                                        fieldName)));
 
+               if (TupleDescAttr(tupdesc, attn - 1)->attgenerated)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+                                        errmsg("cannot set generated column \"%s\"",
+                                                       fieldName)));
+
                values[attn - 1] = utf_u2e(Tcl_GetString(kvObjv[i + 1]));
        }
 
index 277d9a04138f3e5fe307b5963eaceea77fb89e3d..2db75a333a01a0dda5b755449f1a5fec432b3f14 100644 (file)
@@ -71,6 +71,11 @@ CREATE TABLE trigger_test (
 -- Make certain dropped attributes are handled correctly
 ALTER TABLE trigger_test DROP dropme;
 
+CREATE TABLE trigger_test_generated (
+   i int,
+   j int GENERATED ALWAYS AS (i * 2) STORED
+);
+
 CREATE VIEW trigger_test_view AS SELECT i, v FROM trigger_test;
 
 CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
@@ -125,6 +130,13 @@ CREATE TRIGGER statement_trigger
 BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON trigger_test
 FOR EACH STATEMENT EXECUTE PROCEDURE trigger_data(42,'statement trigger');
 
+CREATE TRIGGER show_trigger_data_trig_before
+BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+CREATE TRIGGER show_trigger_data_trig_after
+AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+
 CREATE TRIGGER show_trigger_data_view_trig
 INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
 FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
@@ -531,6 +543,10 @@ select * from T_pkey2 order by key1 using @<, key2 collate "C";
 -- show dump of trigger data
 insert into trigger_test values(1,'insert');
 
+insert into trigger_test_generated (i) values (1);
+update trigger_test_generated set i = 11 where i = 1;
+delete from trigger_test_generated;
+
 insert into trigger_test_view values(2,'insert');
 update trigger_test_view set v = 'update' where i=1;
 delete from trigger_test_view;
@@ -540,6 +556,9 @@ update trigger_test set v = 'update' where i = 1;
 delete from trigger_test;
 truncate trigger_test;
 
+DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
+DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;
+
 -- should error
 insert into trigger_test(test_argisnull) values(true);
 
@@ -565,3 +584,20 @@ CREATE TRIGGER a_t AFTER UPDATE ON transition_table_test
 update transition_table_test set name = 'b';
 drop table transition_table_test;
 drop function transition_table_test_f();
+
+-- dealing with generated columns
+
+CREATE FUNCTION generated_test_func1() RETURNS trigger
+LANGUAGE pltcl
+AS $$
+# not allowed
+set NEW(j) 5
+return [array get NEW]
+$$;
+
+CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
+
+TRUNCATE trigger_test_generated;
+INSERT INTO trigger_test_generated (i) VALUES (1);
+SELECT * FROM trigger_test_generated;
index 4c8a5bd7e2d003097d5c670cc6d391aa59570bd0..b153d6adb1db0ff55256edb454b57c6d75b48efa 100644 (file)
@@ -113,6 +113,52 @@ 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);
+\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
+
+INSERT INTO test_like_gen_1 (a) VALUES (1);
+SELECT * FROM test_like_gen_1;
+ a | b 
+---+---
+ 1 | 2
+(1 row)
+
+CREATE TABLE test_like_gen_2 (LIKE test_like_gen_1);
+\d test_like_gen_2
+          Table "public.test_like_gen_2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+
+INSERT INTO test_like_gen_2 (a) VALUES (1);
+SELECT * FROM test_like_gen_2;
+ a | b 
+---+---
+ 1 |  
+(1 row)
+
+CREATE TABLE test_like_gen_3 (LIKE test_like_gen_1 INCLUDING GENERATED);
+\d test_like_gen_3
+                        Table "public.test_like_gen_3"
+ Column |  Type   | Collation | Nullable |              Default               
+--------+---------+-----------+----------+------------------------------------
+ a      | integer |           |          | 
+ b      | integer |           |          | generated always as (a * 2) stored
+
+INSERT INTO test_like_gen_3 (a) VALUES (1);
+SELECT * FROM test_like_gen_3;
+ a | b 
+---+---
+ 1 | 2
+(1 row)
+
+DROP TABLE test_like_gen_1, test_like_gen_2, test_like_gen_3;
 CREATE TABLE inhg (x text, LIKE inhx INCLUDING INDEXES, y text); /* copies indexes */
 INSERT INTO inhg VALUES (5, 10);
 INSERT INTO inhg VALUES (20, 10); -- should fail
diff --git a/src/test/regress/expected/generated.out b/src/test/regress/expected/generated.out
new file mode 100644 (file)
index 0000000..e0d1d88
--- /dev/null
@@ -0,0 +1,768 @@
+-- sanity check of system catalog
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
+ attrelid | attname | attgenerated 
+----------+---------+--------------
+(0 rows)
+
+CREATE TABLE gtest0 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (55) STORED);
+CREATE TABLE gtest1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+SELECT table_name, column_name, column_default, is_nullable, is_generated, generation_expression FROM information_schema.columns WHERE table_name LIKE 'gtest_' ORDER BY 1, 2;
+ table_name | column_name | column_default | is_nullable | is_generated | generation_expression 
+------------+-------------+----------------+-------------+--------------+-----------------------
+ gtest0     | a           |                | NO          | NEVER        | 
+ gtest0     | b           |                | YES         | ALWAYS       | 55
+ gtest1     | a           |                | NO          | NEVER        | 
+ gtest1     | b           |                | YES         | ALWAYS       | (a * 2)
+(4 rows)
+
+SELECT table_name, column_name, dependent_column FROM information_schema.column_column_usage ORDER BY 1, 2, 3;
+ table_name | column_name | dependent_column 
+------------+-------------+------------------
+ gtest1     | a           | b
+(1 row)
+
+\d gtest1
+                            Table "public.gtest1"
+ Column |  Type   | Collation | Nullable |              Default               
+--------+---------+-----------+----------+------------------------------------
+ a      | integer |           | not null | 
+ b      | integer |           |          | generated always as (a * 2) stored
+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);
+ERROR:  multiple generation clauses specified for column "b" of table "gtest_err_1"
+LINE 1: ...ARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED GENERATED ...
+                                                             ^
+-- references to other generated columns, including self-references
+CREATE TABLE gtest_err_2a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (b * 2) STORED);
+ERROR:  cannot use generated column "b" in column generation expression
+LINE 1: ...2a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (b * 2) STO...
+                                                             ^
+DETAIL:  A generated column cannot reference another generated column.
+CREATE TABLE gtest_err_2b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (b * 3) STORED);
+ERROR:  cannot use generated column "b" in column generation expression
+LINE 1: ...AYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (b * 3) STO...
+                                                             ^
+DETAIL:  A generated column cannot reference another generated column.
+-- invalid reference
+CREATE TABLE gtest_err_3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) STORED);
+ERROR:  column "c" does not exist
+LINE 1: ..._3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) STO...
+                                                             ^
+-- generation expression must be immutable
+CREATE TABLE gtest_err_4 (a int PRIMARY KEY, b double precision GENERATED ALWAYS AS (random()) STORED);
+ERROR:  generation expression is not immutable
+-- cannot have default/identity and generated
+CREATE TABLE gtest_err_5a (a int PRIMARY KEY, b int DEFAULT 5 GENERATED ALWAYS AS (a * 2) STORED);
+ERROR:  both default and generation expression specified for column "b" of table "gtest_err_5a"
+LINE 1: ... gtest_err_5a (a int PRIMARY KEY, b int DEFAULT 5 GENERATED ...
+                                                             ^
+CREATE TABLE gtest_err_5b (a int PRIMARY KEY, b int GENERATED ALWAYS AS identity GENERATED ALWAYS AS (a * 2) STORED);
+ERROR:  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
+CREATE TABLE gtest_err_6a (a int PRIMARY KEY, b bool GENERATED ALWAYS AS (xmin <> 37) STORED);
+ERROR:  cannot use system column "xmin" in column generation expression
+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);
+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...
+                                                             ^
+CREATE TABLE gtest_err_7b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (row_number() OVER (ORDER BY a)) STORED);
+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);
+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);
+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...
+                                                             ^
+INSERT INTO gtest1 VALUES (1);
+INSERT INTO gtest1 VALUES (2, DEFAULT);
+INSERT INTO gtest1 VALUES (3, 33);  -- error
+ERROR:  cannot insert into column "b"
+DETAIL:  Column "b" is a generated column.
+SELECT * FROM gtest1 ORDER BY a;
+ a | b 
+---+---
+ 1 | 2
+ 2 | 4
+(2 rows)
+
+UPDATE gtest1 SET b = DEFAULT WHERE a = 1;
+UPDATE gtest1 SET b = 11 WHERE a = 1;  -- error
+ERROR:  column "b" can only be updated to DEFAULT
+DETAIL:  Column "b" is a generated column.
+SELECT * FROM gtest1 ORDER BY a;
+ a | b 
+---+---
+ 1 | 2
+ 2 | 4
+(2 rows)
+
+SELECT a, b, b * 2 AS b2 FROM gtest1 ORDER BY a;
+ a | b | b2 
+---+---+----
+ 1 | 2 |  4
+ 2 | 4 |  8
+(2 rows)
+
+SELECT a, b FROM gtest1 WHERE b = 4 ORDER BY a;
+ a | b 
+---+---
+ 2 | 4
+(1 row)
+
+-- test that overflow error happens on write
+INSERT INTO gtest1 VALUES (2000000000);
+ERROR:  integer out of range
+SELECT * FROM gtest1;
+ a | b 
+---+---
+ 2 | 4
+ 1 | 2
+(2 rows)
+
+DELETE FROM gtest1 WHERE a = 2000000000;
+-- test with joins
+CREATE TABLE gtestx (x int, y int);
+INSERT INTO gtestx VALUES (11, 1), (22, 2), (33, 3);
+SELECT * FROM gtestx, gtest1 WHERE gtestx.y = gtest1.a;
+ x  | y | a | b 
+----+---+---+---
+ 11 | 1 | 1 | 2
+ 22 | 2 | 2 | 4
+(2 rows)
+
+DROP TABLE gtestx;
+-- test UPDATE/DELETE quals
+SELECT * FROM gtest1 ORDER BY a;
+ a | b 
+---+---
+ 1 | 2
+ 2 | 4
+(2 rows)
+
+UPDATE gtest1 SET a = 3 WHERE b = 4;
+SELECT * FROM gtest1 ORDER BY a;
+ a | b 
+---+---
+ 1 | 2
+ 3 | 6
+(2 rows)
+
+DELETE FROM gtest1 WHERE b = 2;
+SELECT * FROM gtest1 ORDER BY a;
+ a | b 
+---+---
+ 3 | 6
+(1 row)
+
+-- views
+CREATE VIEW gtest1v AS SELECT * FROM gtest1;
+SELECT * FROM gtest1v;
+ a | b 
+---+---
+ 3 | 6
+(1 row)
+
+INSERT INTO gtest1v VALUES (4, 8);  -- fails
+ERROR:  cannot insert into column "b"
+DETAIL:  Column "b" is a generated column.
+DROP VIEW gtest1v;
+-- CTEs
+WITH foo AS (SELECT * FROM gtest1) SELECT * FROM foo;
+ a | b 
+---+---
+ 3 | 6
+(1 row)
+
+-- inheritance
+CREATE TABLE gtest1_1 () INHERITS (gtest1);
+SELECT * FROM gtest1_1;
+ a | b 
+---+---
+(0 rows)
+
+\d gtest1_1
+                           Table "public.gtest1_1"
+ Column |  Type   | Collation | Nullable |              Default               
+--------+---------+-----------+----------+------------------------------------
+ a      | integer |           | not null | 
+ b      | integer |           |          | generated always as (a * 2) stored
+Inherits: gtest1
+
+INSERT INTO gtest1_1 VALUES (4);
+SELECT * FROM gtest1_1;
+ a | b 
+---+---
+ 4 | 8
+(1 row)
+
+SELECT * FROM gtest1;
+ a | b 
+---+---
+ 3 | 6
+ 4 | 8
+(2 rows)
+
+-- test inheritance mismatch
+CREATE TABLE gtesty (x int, b int);
+CREATE TABLE gtest1_2 () INHERITS (gtest1, gtesty);  -- error
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  inherited column "b" has a generation conflict
+DROP TABLE gtesty;
+-- test stored update
+CREATE TABLE gtest3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 3) STORED);
+INSERT INTO gtest3 (a) VALUES (1), (2), (3);
+SELECT * FROM gtest3 ORDER BY a;
+ a | b 
+---+---
+ 1 | 3
+ 2 | 6
+ 3 | 9
+(3 rows)
+
+UPDATE gtest3 SET a = 22 WHERE a = 2;
+SELECT * FROM gtest3 ORDER BY a;
+ a  | b  
+----+----
+  1 |  3
+  3 |  9
+ 22 | 66
+(3 rows)
+
+-- COPY
+TRUNCATE gtest1;
+INSERT INTO gtest1 (a) VALUES (1), (2);
+COPY gtest1 TO stdout;
+1
+2
+COPY gtest1 (a, b) TO stdout;
+ERROR:  column "b" is a generated column
+DETAIL:  Generated columns cannot be used in COPY.
+COPY gtest1 FROM stdin;
+COPY gtest1 (a, b) FROM stdin;
+ERROR:  column "b" is a generated column
+DETAIL:  Generated columns cannot be used in COPY.
+SELECT * FROM gtest1 ORDER BY a;
+ a | b 
+---+---
+ 1 | 2
+ 2 | 4
+ 3 | 6
+ 4 | 8
+(4 rows)
+
+TRUNCATE gtest3;
+INSERT INTO gtest3 (a) VALUES (1), (2);
+COPY gtest3 TO stdout;
+1
+2
+COPY gtest3 (a, b) TO stdout;
+ERROR:  column "b" is a generated column
+DETAIL:  Generated columns cannot be used in COPY.
+COPY gtest3 FROM stdin;
+COPY gtest3 (a, b) FROM stdin;
+ERROR:  column "b" is a generated column
+DETAIL:  Generated columns cannot be used in COPY.
+SELECT * FROM gtest3 ORDER BY a;
+ a | b  
+---+----
+ 1 |  3
+ 2 |  6
+ 3 |  9
+ 4 | 12
+(4 rows)
+
+-- null values
+CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED);
+INSERT INTO gtest2 VALUES (1);
+SELECT * FROM gtest2;
+ a | b 
+---+---
+ 1 |  
+(1 row)
+
+-- composite types
+CREATE TYPE double_int as (a int, b int);
+CREATE TABLE gtest4 (
+    a int,
+    b double_int GENERATED ALWAYS AS ((a * 2, a * 3)) STORED
+);
+INSERT INTO gtest4 VALUES (1), (6);
+SELECT * FROM gtest4;
+ a |    b    
+---+---------
+ 1 | (2,3)
+ 6 | (12,18)
+(2 rows)
+
+DROP TABLE gtest4;
+DROP TYPE double_int;
+-- using tableoid is allowed
+CREATE TABLE gtest_tableoid (
+  a int PRIMARY KEY,
+  b bool GENERATED ALWAYS AS (tableoid <> 0) STORED
+);
+INSERT INTO gtest_tableoid VALUES (1), (2);
+SELECT * FROM gtest_tableoid;
+ a | b 
+---+---
+ 1 | t
+ 2 | t
+(2 rows)
+
+-- drop column behavior
+CREATE TABLE gtest10 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) STORED);
+ALTER TABLE gtest10 DROP COLUMN b;
+\d gtest10
+              Table "public.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);
+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 FUNCTION gf1(a int) RETURNS int AS $$ SELECT a * 3 $$ IMMUTABLE LANGUAGE SQL;
+REVOKE ALL ON FUNCTION gf1(int) FROM PUBLIC;
+CREATE TABLE gtest12s (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) STORED);
+INSERT INTO gtest12s VALUES (1, 10), (2, 20);
+GRANT SELECT (a, c) ON gtest12s TO regress_user11;
+SET ROLE regress_user11;
+SELECT a, b FROM gtest11s;  -- not allowed
+ERROR:  permission denied for table gtest11s
+SELECT a, c FROM gtest11s;  -- allowed
+ a | c  
+---+----
+ 1 | 20
+ 2 | 40
+(2 rows)
+
+SELECT gf1(10);  -- not allowed
+ERROR:  permission denied for function gf1
+SELECT a, c FROM gtest12s;  -- allowed
+ a | c  
+---+----
+ 1 | 30
+ 2 | 60
+(2 rows)
+
+RESET ROLE;
+DROP TABLE gtest11s, gtest12s;
+DROP FUNCTION gf1(int);
+DROP USER regress_user11;
+-- check constraints
+CREATE TABLE gtest20 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED CHECK (b < 50));
+INSERT INTO gtest20 (a) VALUES (10);  -- ok
+INSERT INTO gtest20 (a) VALUES (30);  -- violates constraint
+ERROR:  new row for relation "gtest20" violates check constraint "gtest20_b_check"
+DETAIL:  Failing row contains (30, 60).
+CREATE TABLE gtest20a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+INSERT INTO gtest20a (a) VALUES (10);
+INSERT INTO gtest20a (a) VALUES (30);
+ALTER TABLE gtest20a ADD CHECK (b < 50);  -- fails on existing row
+ERROR:  check constraint "gtest20a_b_check" is violated by some row
+CREATE TABLE gtest20b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+INSERT INTO gtest20b (a) VALUES (10);
+INSERT INTO gtest20b (a) VALUES (30);
+ALTER TABLE gtest20b ADD CONSTRAINT chk CHECK (b < 50) NOT VALID;
+ALTER TABLE gtest20b VALIDATE CONSTRAINT chk;  -- fails on existing row
+ERROR:  check constraint "chk" is violated by some row
+-- 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" 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);
+ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL;
+INSERT INTO gtest21b (a) VALUES (1);  -- ok
+INSERT INTO gtest21b (a) VALUES (0);  -- violates constraint
+ERROR:  null value in column "b" violates not-null constraint
+DETAIL:  Failing row contains (0, null).
+ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
+INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+-- 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.
+-- indexes
+CREATE TABLE gtest22c (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE INDEX gtest22c_b_idx ON gtest22c (b);
+CREATE INDEX gtest22c_expr_idx ON gtest22c ((b * 3));
+CREATE INDEX gtest22c_pred_idx ON gtest22c (a) WHERE b > 0;
+\d gtest22c
+                           Table "public.gtest22c"
+ Column |  Type   | 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)
+
+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
+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
+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 "public.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".
+DROP TABLE gtest23b;
+DROP TABLE gtest23a;
+CREATE TABLE gtest23p (x int, y int GENERATED ALWAYS AS (x * 2) STORED, PRIMARY KEY (y));
+INSERT INTO gtest23p VALUES (1), (2), (3);
+CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y));
+INSERT INTO gtest23q VALUES (1, 2);  -- ok
+INSERT INTO gtest23q VALUES (2, 5);  -- error
+ERROR:  insert or update on table "gtest23q" violates foreign key constraint "gtest23q_b_fkey"
+DETAIL:  Key (b)=(5) is not present in table "gtest23p".
+-- 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"
+-- typed tables (currently not supported)
+CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint);
+CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) STORED);
+ERROR:  generated columns are not supported on typed tables
+DROP TYPE gtest_type CASCADE;
+-- table partitions (currently not supported)
+CREATE TABLE gtest_parent (f1 date NOT NULL, f2 text, f3 bigint) PARTITION BY RANGE (f1);
+CREATE TABLE gtest_child PARTITION OF gtest_parent (
+    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 2) STORED
+) FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- error
+ERROR:  generated columns are not supported on partitions
+DROP TABLE gtest_parent;
+-- partitioned table
+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_child PARTITION OF gtest_parent FOR VALUES FROM ('2016-07-01') TO ('2016-08-01');
+INSERT INTO gtest_parent (f1, f2) VALUES ('2016-07-15', 1);
+SELECT * FROM gtest_parent;
+     f1     | f2 | f3 
+------------+----+----
+ 07-15-2016 |  1 |  2
+(1 row)
+
+SELECT * FROM gtest_child;
+     f1     | f2 | f3 
+------------+----+----
+ 07-15-2016 |  1 |  2
+(1 row)
+
+DROP TABLE gtest_parent;
+-- generated columns in partition key (not allowed)
+CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f3);
+ERROR:  cannot use generated column in partition key
+LINE 1: ...ENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f3);
+                                                                   ^
+DETAIL:  Column "f3" is a generated column.
+CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) 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));
+                                                             ^
+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 * 3) STORED;
+SELECT * FROM gtest25 ORDER BY a;
+ a | b  
+---+----
+ 3 |  9
+ 4 | 12
+(2 rows)
+
+ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (b * 4) STORED;  -- error
+ERROR:  cannot use generated column "b" in column generation expression
+DETAIL:  A generated column cannot reference another generated column.
+ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (z * 4) STORED;  -- error
+ERROR:  column "z" does not exist
+-- ALTER TABLE ... ALTER COLUMN
+CREATE TABLE gtest27 (
+    a int,
+    b int GENERATED ALWAYS AS (a * 2) STORED
+);
+INSERT INTO gtest27 (a) VALUES (3), (4);
+ALTER TABLE gtest27 ALTER COLUMN a TYPE text;  -- error
+ERROR:  cannot alter type of a column used by a generated column
+DETAIL:  Column "a" is used by generated column "b".
+ALTER TABLE gtest27 ALTER COLUMN b TYPE numeric;
+\d gtest27
+                             Table "public.gtest27"
+ Column |  Type   | Collation | Nullable |               Default                
+--------+---------+-----------+----------+--------------------------------------
+ a      | integer |           |          | 
+ b      | numeric |           |          | generated always as ((a * 2)) stored
+
+SELECT * FROM gtest27;
+ a | b 
+---+---
+ 3 | 6
+ 4 | 8
+(2 rows)
+
+ALTER TABLE gtest27 ALTER COLUMN b TYPE boolean USING b <> 0;  -- error
+ERROR:  generation expression for column "b" cannot be cast automatically to type boolean
+ALTER TABLE gtest27 ALTER COLUMN b DROP DEFAULT;  -- error
+ERROR:  column "b" of relation "gtest27" is a generated column
+\d gtest27
+                             Table "public.gtest27"
+ Column |  Type   | Collation | Nullable |               Default                
+--------+---------+-----------+----------+--------------------------------------
+ a      | integer |           |          | 
+ b      | numeric |           |          | generated always as ((a * 2)) stored
+
+-- triggers
+CREATE TABLE gtest26 (
+    a int PRIMARY KEY,
+    b int GENERATED ALWAYS AS (a * 2) STORED
+);
+CREATE FUNCTION gtest_trigger_func() RETURNS trigger
+  LANGUAGE plpgsql
+AS $$
+BEGIN
+  IF tg_op IN ('DELETE', 'UPDATE') THEN
+    RAISE INFO '%: %: old = %', TG_NAME, TG_WHEN, OLD;
+  END IF;
+  IF tg_op IN ('INSERT', 'UPDATE') THEN
+    RAISE INFO '%: %: new = %', TG_NAME, TG_WHEN, NEW;
+  END IF;
+  IF tg_op = 'DELETE' THEN
+    RETURN OLD;
+  ELSE
+    RETURN NEW;
+  END IF;
+END
+$$;
+CREATE TRIGGER gtest1 BEFORE DELETE OR UPDATE ON gtest26
+  FOR EACH ROW
+  WHEN (OLD.b < 0)  -- ok
+  EXECUTE PROCEDURE gtest_trigger_func();
+CREATE TRIGGER gtest2a BEFORE INSERT OR UPDATE ON gtest26
+  FOR EACH ROW
+  WHEN (NEW.b < 0)  -- error
+  EXECUTE PROCEDURE gtest_trigger_func();
+ERROR:  BEFORE trigger's WHEN condition cannot reference NEW generated columns
+LINE 3:   WHEN (NEW.b < 0)  
+                ^
+DETAIL:  Column "b" is a generated column.
+CREATE TRIGGER gtest2b BEFORE INSERT OR UPDATE ON gtest26
+  FOR EACH ROW
+  WHEN (NEW.* IS NOT NULL)  -- error
+  EXECUTE PROCEDURE gtest_trigger_func();
+ERROR:  BEFORE trigger's WHEN condition cannot reference NEW generated columns
+LINE 3:   WHEN (NEW.* IS NOT NULL)  
+                ^
+DETAIL:  A whole-row reference is used and the table contains generated columns.
+CREATE TRIGGER gtest2 BEFORE INSERT ON gtest26
+  FOR EACH ROW
+  WHEN (NEW.a < 0)
+  EXECUTE PROCEDURE gtest_trigger_func();
+CREATE TRIGGER gtest3 AFTER DELETE OR UPDATE ON gtest26
+  FOR EACH ROW
+  WHEN (OLD.b < 0)  -- ok
+  EXECUTE PROCEDURE gtest_trigger_func();
+CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26
+  FOR EACH ROW
+  WHEN (NEW.b < 0)  -- ok
+  EXECUTE PROCEDURE gtest_trigger_func();
+INSERT INTO gtest26 (a) VALUES (-2), (0), (3);
+INFO:  gtest2: BEFORE: new = (-2,)
+INFO:  gtest4: AFTER: new = (-2,-4)
+SELECT * FROM gtest26 ORDER BY a;
+ a  | b  
+----+----
+ -2 | -4
+  0 |  0
+  3 |  6
+(3 rows)
+
+UPDATE gtest26 SET a = a * -2;
+INFO:  gtest1: BEFORE: old = (-2,-4)
+INFO:  gtest1: BEFORE: new = (4,)
+INFO:  gtest3: AFTER: old = (-2,-4)
+INFO:  gtest3: AFTER: new = (4,8)
+INFO:  gtest4: AFTER: old = (3,6)
+INFO:  gtest4: AFTER: new = (-6,-12)
+SELECT * FROM gtest26 ORDER BY a;
+ a  |  b  
+----+-----
+ -6 | -12
+  0 |   0
+  4 |   8
+(3 rows)
+
+DELETE FROM gtest26 WHERE a = -6;
+INFO:  gtest1: BEFORE: old = (-6,-12)
+INFO:  gtest3: AFTER: old = (-6,-12)
+SELECT * FROM gtest26 ORDER BY a;
+ a | b 
+---+---
+ 0 | 0
+ 4 | 8
+(2 rows)
+
+DROP TRIGGER gtest1 ON gtest26;
+DROP TRIGGER gtest2 ON gtest26;
+DROP TRIGGER gtest3 ON gtest26;
+-- Check that an UPDATE of "a" fires the trigger for UPDATE OF b, per
+-- SQL standard.
+CREATE FUNCTION gtest_trigger_func3() RETURNS trigger
+  LANGUAGE plpgsql
+AS $$
+BEGIN
+  RAISE NOTICE 'OK';
+  RETURN NEW;
+END
+$$;
+CREATE TRIGGER gtest11 BEFORE UPDATE OF b ON gtest26
+  FOR EACH ROW
+  EXECUTE PROCEDURE gtest_trigger_func3();
+UPDATE gtest26 SET a = 1 WHERE a = 0;
+NOTICE:  OK
+DROP TRIGGER gtest11 ON gtest26;
+TRUNCATE gtest26;
+-- check that modifications of stored generated columns in triggers do
+-- not get propagated
+CREATE FUNCTION gtest_trigger_func4() RETURNS trigger
+  LANGUAGE plpgsql
+AS $$
+BEGIN
+  NEW.a = 10;
+  NEW.b = 300;
+  RETURN NEW;
+END;
+$$;
+CREATE TRIGGER gtest12_01 BEFORE UPDATE ON gtest26
+  FOR EACH ROW
+  EXECUTE PROCEDURE gtest_trigger_func();
+CREATE TRIGGER gtest12_02 BEFORE UPDATE ON gtest26
+  FOR EACH ROW
+  EXECUTE PROCEDURE gtest_trigger_func4();
+CREATE TRIGGER gtest12_03 BEFORE UPDATE ON gtest26
+  FOR EACH ROW
+  EXECUTE PROCEDURE gtest_trigger_func();
+INSERT INTO gtest26 (a) VALUES (1);
+UPDATE gtest26 SET a = 11 WHERE a = 1;
+INFO:  gtest12_01: BEFORE: old = (1,2)
+INFO:  gtest12_01: BEFORE: new = (11,)
+INFO:  gtest12_03: BEFORE: old = (1,2)
+INFO:  gtest12_03: BEFORE: new = (10,)
+SELECT * FROM gtest26 ORDER BY a;
+ a  | b  
+----+----
+ 10 | 20
+(1 row)
+
+-- LIKE INCLUDING GENERATED and dropped column handling
+CREATE TABLE gtest28a (
+  a int,
+  b int,
+  c int,
+  x int GENERATED ALWAYS AS (b * 2) STORED
+);
+ALTER TABLE gtest28a DROP COLUMN a;
+CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
+\d gtest28*
+                           Table "public.gtest28a"
+ Column |  Type   | Collation | Nullable |              Default               
+--------+---------+-----------+----------+------------------------------------
+ b      | integer |           |          | 
+ c      | integer |           |          | 
+ x      | integer |           |          | generated always as (b * 2) stored
+
+                           Table "public.gtest28b"
+ Column |  Type   | Collation | Nullable |              Default               
+--------+---------+-----------+----------+------------------------------------
+ b      | integer |           |          | 
+ c      | integer |           |          | 
+ x      | integer |           |          | generated always as (b * 2) stored
+
index 908fbf650aa7a1ee70f720ef5de831d5d75ddb12..f320fb6ef3a9dc6437cf2a34c50db4874fbd9414 100644 (file)
@@ -84,7 +84,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password identity
+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
 
 # ----------
 # Another group of parallel tests
index fa754d1c6b0367fb17d442eeafc76b8e7520c9de..36644aa9636f0fb892e29696c98ecba1006c4262 100644 (file)
@@ -122,6 +122,7 @@ test: groupingsets
 test: drop_operator
 test: password
 test: identity
+test: generated
 test: create_table_like
 test: alter_generic
 test: alter_operator
index 65c3880792b621eb059e470a924456c1c2a888df..9b19c680b50b7547a46d467e945cec397d149afd 100644 (file)
@@ -51,6 +51,20 @@ INSERT INTO test_like_id_3 (b) VALUES ('b3');
 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);
+\d test_like_gen_1
+INSERT INTO test_like_gen_1 (a) VALUES (1);
+SELECT * FROM test_like_gen_1;
+CREATE TABLE test_like_gen_2 (LIKE test_like_gen_1);
+\d test_like_gen_2
+INSERT INTO test_like_gen_2 (a) VALUES (1);
+SELECT * FROM test_like_gen_2;
+CREATE TABLE test_like_gen_3 (LIKE test_like_gen_1 INCLUDING GENERATED);
+\d test_like_gen_3
+INSERT INTO test_like_gen_3 (a) VALUES (1);
+SELECT * FROM test_like_gen_3;
+DROP TABLE test_like_gen_1, test_like_gen_2, test_like_gen_3;
+
 CREATE TABLE inhg (x text, LIKE inhx INCLUDING INDEXES, y text); /* copies indexes */
 INSERT INTO inhg VALUES (5, 10);
 INSERT INTO inhg VALUES (20, 10); -- should fail
diff --git a/src/test/regress/sql/generated.sql b/src/test/regress/sql/generated.sql
new file mode 100644 (file)
index 0000000..6d71594
--- /dev/null
@@ -0,0 +1,451 @@
+-- sanity check of system catalog
+SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's');
+
+
+CREATE TABLE gtest0 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (55) STORED);
+CREATE TABLE gtest1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+
+SELECT table_name, column_name, column_default, is_nullable, is_generated, generation_expression FROM information_schema.columns WHERE table_name LIKE 'gtest_' ORDER BY 1, 2;
+
+SELECT table_name, column_name, dependent_column FROM information_schema.column_column_usage 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);
+
+-- references to other generated columns, including self-references
+CREATE TABLE gtest_err_2a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (b * 2) STORED);
+CREATE TABLE gtest_err_2b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED, c int GENERATED ALWAYS AS (b * 3) STORED);
+
+-- invalid reference
+CREATE TABLE gtest_err_3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (c * 2) STORED);
+
+-- generation expression must be immutable
+CREATE TABLE gtest_err_4 (a int PRIMARY KEY, b double precision GENERATED ALWAYS AS (random()) STORED);
+
+-- cannot have default/identity and generated
+CREATE TABLE gtest_err_5a (a int PRIMARY KEY, b int DEFAULT 5 GENERATED ALWAYS AS (a * 2) STORED);
+CREATE TABLE gtest_err_5b (a int PRIMARY KEY, b int GENERATED ALWAYS AS identity GENERATED ALWAYS AS (a * 2) STORED);
+
+-- reference to system column not allowed in generated column
+CREATE TABLE gtest_err_6a (a int PRIMARY KEY, b bool GENERATED ALWAYS AS (xmin <> 37) STORED);
+
+-- various prohibited constructs
+CREATE TABLE gtest_err_7a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (avg(a)) STORED);
+CREATE TABLE gtest_err_7b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (row_number() OVER (ORDER BY a)) STORED);
+CREATE TABLE gtest_err_7c (a int PRIMARY KEY, b int GENERATED ALWAYS AS ((SELECT a)) STORED);
+CREATE TABLE gtest_err_7d (a int PRIMARY KEY, b int GENERATED ALWAYS AS (generate_series(1, a)) STORED);
+
+INSERT INTO gtest1 VALUES (1);
+INSERT INTO gtest1 VALUES (2, DEFAULT);
+INSERT INTO gtest1 VALUES (3, 33);  -- error
+
+SELECT * FROM gtest1 ORDER BY a;
+
+UPDATE gtest1 SET b = DEFAULT WHERE a = 1;
+UPDATE gtest1 SET b = 11 WHERE a = 1;  -- error
+
+SELECT * FROM gtest1 ORDER BY a;
+
+SELECT a, b, b * 2 AS b2 FROM gtest1 ORDER BY a;
+SELECT a, b FROM gtest1 WHERE b = 4 ORDER BY a;
+
+-- test that overflow error happens on write
+INSERT INTO gtest1 VALUES (2000000000);
+SELECT * FROM gtest1;
+DELETE FROM gtest1 WHERE a = 2000000000;
+
+-- test with joins
+CREATE TABLE gtestx (x int, y int);
+INSERT INTO gtestx VALUES (11, 1), (22, 2), (33, 3);
+SELECT * FROM gtestx, gtest1 WHERE gtestx.y = gtest1.a;
+DROP TABLE gtestx;
+
+-- test UPDATE/DELETE quals
+SELECT * FROM gtest1 ORDER BY a;
+UPDATE gtest1 SET a = 3 WHERE b = 4;
+SELECT * FROM gtest1 ORDER BY a;
+DELETE FROM gtest1 WHERE b = 2;
+SELECT * FROM gtest1 ORDER BY a;
+
+-- views
+CREATE VIEW gtest1v AS SELECT * FROM gtest1;
+SELECT * FROM gtest1v;
+INSERT INTO gtest1v VALUES (4, 8);  -- fails
+DROP VIEW gtest1v;
+
+-- CTEs
+WITH foo AS (SELECT * FROM gtest1) SELECT * FROM foo;
+
+-- inheritance
+CREATE TABLE gtest1_1 () INHERITS (gtest1);
+SELECT * FROM gtest1_1;
+\d gtest1_1
+INSERT INTO gtest1_1 VALUES (4);
+SELECT * FROM gtest1_1;
+SELECT * FROM gtest1;
+
+-- test inheritance mismatch
+CREATE TABLE gtesty (x int, b int);
+CREATE TABLE gtest1_2 () INHERITS (gtest1, gtesty);  -- error
+DROP TABLE gtesty;
+
+-- test stored update
+CREATE TABLE gtest3 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 3) STORED);
+INSERT INTO gtest3 (a) VALUES (1), (2), (3);
+SELECT * FROM gtest3 ORDER BY a;
+UPDATE gtest3 SET a = 22 WHERE a = 2;
+SELECT * FROM gtest3 ORDER BY a;
+
+-- COPY
+TRUNCATE gtest1;
+INSERT INTO gtest1 (a) VALUES (1), (2);
+
+COPY gtest1 TO stdout;
+
+COPY gtest1 (a, b) TO stdout;
+
+COPY gtest1 FROM stdin;
+3
+4
+\.
+
+COPY gtest1 (a, b) FROM stdin;
+
+SELECT * FROM gtest1 ORDER BY a;
+
+TRUNCATE gtest3;
+INSERT INTO gtest3 (a) VALUES (1), (2);
+
+COPY gtest3 TO stdout;
+
+COPY gtest3 (a, b) TO stdout;
+
+COPY gtest3 FROM stdin;
+3
+4
+\.
+
+COPY gtest3 (a, b) FROM stdin;
+
+SELECT * FROM gtest3 ORDER BY a;
+
+-- null values
+CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED);
+INSERT INTO gtest2 VALUES (1);
+SELECT * FROM gtest2;
+
+-- composite types
+CREATE TYPE double_int as (a int, b int);
+CREATE TABLE gtest4 (
+    a int,
+    b double_int GENERATED ALWAYS AS ((a * 2, a * 3)) STORED
+);
+INSERT INTO gtest4 VALUES (1), (6);
+SELECT * FROM gtest4;
+
+DROP TABLE gtest4;
+DROP TYPE double_int;
+
+-- using tableoid is allowed
+CREATE TABLE gtest_tableoid (
+  a int PRIMARY KEY,
+  b bool GENERATED ALWAYS AS (tableoid <> 0) STORED
+);
+INSERT INTO gtest_tableoid VALUES (1), (2);
+SELECT * FROM gtest_tableoid;
+
+-- drop column behavior
+CREATE TABLE gtest10 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (b * 2) STORED);
+ALTER TABLE gtest10 DROP COLUMN b;
+
+\d gtest10
+
+CREATE TABLE gtest10a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+ALTER TABLE gtest10a DROP COLUMN b;
+INSERT INTO gtest10a (a) VALUES (1);
+
+-- 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 FUNCTION gf1(a int) RETURNS int AS $$ SELECT a * 3 $$ IMMUTABLE LANGUAGE SQL;
+REVOKE ALL ON FUNCTION gf1(int) FROM PUBLIC;
+
+CREATE TABLE gtest12s (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) STORED);
+INSERT INTO gtest12s VALUES (1, 10), (2, 20);
+GRANT SELECT (a, c) ON gtest12s TO regress_user11;
+
+SET ROLE regress_user11;
+SELECT a, b FROM gtest11s;  -- not allowed
+SELECT a, c FROM gtest11s;  -- allowed
+SELECT gf1(10);  -- not allowed
+SELECT a, c FROM gtest12s;  -- allowed
+RESET ROLE;
+
+DROP TABLE gtest11s, gtest12s;
+DROP FUNCTION gf1(int);
+DROP USER regress_user11;
+
+-- check constraints
+CREATE TABLE gtest20 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED CHECK (b < 50));
+INSERT INTO gtest20 (a) VALUES (10);  -- ok
+INSERT INTO gtest20 (a) VALUES (30);  -- violates constraint
+
+CREATE TABLE gtest20a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+INSERT INTO gtest20a (a) VALUES (10);
+INSERT INTO gtest20a (a) VALUES (30);
+ALTER TABLE gtest20a ADD CHECK (b < 50);  -- fails on existing row
+
+CREATE TABLE gtest20b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED);
+INSERT INTO gtest20b (a) VALUES (10);
+INSERT INTO gtest20b (a) VALUES (30);
+ALTER TABLE gtest20b ADD CONSTRAINT chk CHECK (b < 50) NOT VALID;
+ALTER TABLE gtest20b VALIDATE CONSTRAINT chk;  -- fails on existing row
+
+-- not-null constraints
+CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED NOT NULL);
+INSERT INTO gtest21a (a) VALUES (1);  -- ok
+INSERT INTO gtest21a (a) VALUES (0);  -- violates constraint
+
+CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED);
+ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL;
+INSERT INTO gtest21b (a) VALUES (1);  -- ok
+INSERT INTO gtest21b (a) VALUES (0);  -- violates constraint
+ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL;
+INSERT INTO gtest21b (a) VALUES (0);  -- ok now
+
+-- 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);
+
+-- indexes
+CREATE TABLE gtest22c (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
+CREATE INDEX gtest22c_b_idx ON gtest22c (b);
+CREATE INDEX gtest22c_expr_idx ON gtest22c ((b * 3));
+CREATE INDEX gtest22c_pred_idx ON gtest22c (a) WHERE b > 0;
+\d gtest22c
+
+INSERT INTO gtest22c VALUES (1), (2), (3);
+SET enable_seqscan TO off;
+SET enable_bitmapscan TO off;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b = 4;
+SELECT * FROM gtest22c WHERE b = 4;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE b * 3 = 6;
+SELECT * FROM gtest22c WHERE b * 3 = 6;
+EXPLAIN (COSTS OFF) SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
+SELECT * FROM gtest22c WHERE a = 1 AND b > 0;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+-- foreign keys
+CREATE TABLE gtest23a (x int PRIMARY KEY, y int);
+INSERT INTO gtest23a VALUES (1, 11), (2, 22), (3, 33);
+
+CREATE TABLE gtest23x (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED REFERENCES gtest23a (x) ON UPDATE CASCADE);  -- error
+CREATE TABLE gtest23x (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED REFERENCES gtest23a (x) ON DELETE SET NULL);  -- error
+
+CREATE TABLE gtest23b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED REFERENCES gtest23a (x));
+\d gtest23b
+
+INSERT INTO gtest23b VALUES (1);  -- ok
+INSERT INTO gtest23b VALUES (5);  -- error
+
+DROP TABLE gtest23b;
+DROP TABLE gtest23a;
+
+CREATE TABLE gtest23p (x int, y int GENERATED ALWAYS AS (x * 2) STORED, PRIMARY KEY (y));
+INSERT INTO gtest23p VALUES (1), (2), (3);
+
+CREATE TABLE gtest23q (a int PRIMARY KEY, b int REFERENCES gtest23p (y));
+INSERT INTO gtest23q VALUES (1, 2);  -- ok
+INSERT INTO gtest23q VALUES (2, 5);  -- error
+
+-- 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
+
+-- typed tables (currently not supported)
+CREATE TYPE gtest_type AS (f1 integer, f2 text, f3 bigint);
+CREATE TABLE gtest28 OF gtest_type (f1 WITH OPTIONS GENERATED ALWAYS AS (f2 *2) STORED);
+DROP TYPE gtest_type CASCADE;
+
+-- table partitions (currently not supported)
+CREATE TABLE gtest_parent (f1 date NOT NULL, f2 text, f3 bigint) PARTITION BY RANGE (f1);
+CREATE TABLE gtest_child PARTITION OF gtest_parent (
+    f3 WITH OPTIONS GENERATED ALWAYS AS (f2 * 2) STORED
+) FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- error
+DROP TABLE gtest_parent;
+
+-- partitioned table
+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_child PARTITION OF gtest_parent FOR VALUES FROM ('2016-07-01') TO ('2016-08-01');
+INSERT INTO gtest_parent (f1, f2) VALUES ('2016-07-15', 1);
+SELECT * FROM gtest_parent;
+SELECT * FROM gtest_child;
+DROP TABLE gtest_parent;
+
+-- generated columns in partition key (not allowed)
+CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f3);
+CREATE TABLE gtest_parent (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) 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 * 3) STORED;
+SELECT * FROM gtest25 ORDER BY a;
+ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (b * 4) STORED;  -- error
+ALTER TABLE gtest25 ADD COLUMN x int GENERATED ALWAYS AS (z * 4) STORED;  -- error
+
+-- ALTER TABLE ... ALTER COLUMN
+CREATE TABLE gtest27 (
+    a int,
+    b int GENERATED ALWAYS AS (a * 2) STORED
+);
+INSERT INTO gtest27 (a) VALUES (3), (4);
+ALTER TABLE gtest27 ALTER COLUMN a TYPE text;  -- error
+ALTER TABLE gtest27 ALTER COLUMN b TYPE numeric;
+\d gtest27
+SELECT * FROM gtest27;
+ALTER TABLE gtest27 ALTER COLUMN b TYPE boolean USING b <> 0;  -- error
+
+ALTER TABLE gtest27 ALTER COLUMN b DROP DEFAULT;  -- error
+\d gtest27
+
+-- triggers
+CREATE TABLE gtest26 (
+    a int PRIMARY KEY,
+    b int GENERATED ALWAYS AS (a * 2) STORED
+);
+
+CREATE FUNCTION gtest_trigger_func() RETURNS trigger
+  LANGUAGE plpgsql
+AS $$
+BEGIN
+  IF tg_op IN ('DELETE', 'UPDATE') THEN
+    RAISE INFO '%: %: old = %', TG_NAME, TG_WHEN, OLD;
+  END IF;
+  IF tg_op IN ('INSERT', 'UPDATE') THEN
+    RAISE INFO '%: %: new = %', TG_NAME, TG_WHEN, NEW;
+  END IF;
+  IF tg_op = 'DELETE' THEN
+    RETURN OLD;
+  ELSE
+    RETURN NEW;
+  END IF;
+END
+$$;
+
+CREATE TRIGGER gtest1 BEFORE DELETE OR UPDATE ON gtest26
+  FOR EACH ROW
+  WHEN (OLD.b < 0)  -- ok
+  EXECUTE PROCEDURE gtest_trigger_func();
+
+CREATE TRIGGER gtest2a BEFORE INSERT OR UPDATE ON gtest26
+  FOR EACH ROW
+  WHEN (NEW.b < 0)  -- error
+  EXECUTE PROCEDURE gtest_trigger_func();
+
+CREATE TRIGGER gtest2b BEFORE INSERT OR UPDATE ON gtest26
+  FOR EACH ROW
+  WHEN (NEW.* IS NOT NULL)  -- error
+  EXECUTE PROCEDURE gtest_trigger_func();
+
+CREATE TRIGGER gtest2 BEFORE INSERT ON gtest26
+  FOR EACH ROW
+  WHEN (NEW.a < 0)
+  EXECUTE PROCEDURE gtest_trigger_func();
+
+CREATE TRIGGER gtest3 AFTER DELETE OR UPDATE ON gtest26
+  FOR EACH ROW
+  WHEN (OLD.b < 0)  -- ok
+  EXECUTE PROCEDURE gtest_trigger_func();
+
+CREATE TRIGGER gtest4 AFTER INSERT OR UPDATE ON gtest26
+  FOR EACH ROW
+  WHEN (NEW.b < 0)  -- ok
+  EXECUTE PROCEDURE gtest_trigger_func();
+
+INSERT INTO gtest26 (a) VALUES (-2), (0), (3);
+SELECT * FROM gtest26 ORDER BY a;
+UPDATE gtest26 SET a = a * -2;
+SELECT * FROM gtest26 ORDER BY a;
+DELETE FROM gtest26 WHERE a = -6;
+SELECT * FROM gtest26 ORDER BY a;
+
+DROP TRIGGER gtest1 ON gtest26;
+DROP TRIGGER gtest2 ON gtest26;
+DROP TRIGGER gtest3 ON gtest26;
+
+-- Check that an UPDATE of "a" fires the trigger for UPDATE OF b, per
+-- SQL standard.
+CREATE FUNCTION gtest_trigger_func3() RETURNS trigger
+  LANGUAGE plpgsql
+AS $$
+BEGIN
+  RAISE NOTICE 'OK';
+  RETURN NEW;
+END
+$$;
+
+CREATE TRIGGER gtest11 BEFORE UPDATE OF b ON gtest26
+  FOR EACH ROW
+  EXECUTE PROCEDURE gtest_trigger_func3();
+
+UPDATE gtest26 SET a = 1 WHERE a = 0;
+
+DROP TRIGGER gtest11 ON gtest26;
+TRUNCATE gtest26;
+
+-- check that modifications of stored generated columns in triggers do
+-- not get propagated
+CREATE FUNCTION gtest_trigger_func4() RETURNS trigger
+  LANGUAGE plpgsql
+AS $$
+BEGIN
+  NEW.a = 10;
+  NEW.b = 300;
+  RETURN NEW;
+END;
+$$;
+
+CREATE TRIGGER gtest12_01 BEFORE UPDATE ON gtest26
+  FOR EACH ROW
+  EXECUTE PROCEDURE gtest_trigger_func();
+
+CREATE TRIGGER gtest12_02 BEFORE UPDATE ON gtest26
+  FOR EACH ROW
+  EXECUTE PROCEDURE gtest_trigger_func4();
+
+CREATE TRIGGER gtest12_03 BEFORE UPDATE ON gtest26
+  FOR EACH ROW
+  EXECUTE PROCEDURE gtest_trigger_func();
+
+INSERT INTO gtest26 (a) VALUES (1);
+UPDATE gtest26 SET a = 11 WHERE a = 1;
+SELECT * FROM gtest26 ORDER BY a;
+
+-- LIKE INCLUDING GENERATED and dropped column handling
+CREATE TABLE gtest28a (
+  a int,
+  b int,
+  c int,
+  x int GENERATED ALWAYS AS (b * 2) STORED
+);
+
+ALTER TABLE gtest28a DROP COLUMN a;
+
+CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
+
+\d gtest28*
diff --git a/src/test/subscription/t/011_generated.pl b/src/test/subscription/t/011_generated.pl
new file mode 100644 (file)
index 0000000..f7456e9
--- /dev/null
@@ -0,0 +1,65 @@
+# Test generated columns
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 2;
+
+# setup
+
+my $node_publisher = get_new_node('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+my $node_subscriber = get_new_node('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+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)");
+
+$node_subscriber->safe_psql('postgres',
+       "CREATE TABLE tab1 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 22) STORED)");
+
+# data for initial sync
+
+$node_publisher->safe_psql('postgres',
+                                                  "INSERT INTO tab1 (a) VALUES (1), (2), (3)");
+
+$node_publisher->safe_psql('postgres',
+       "CREATE PUBLICATION pub1 FOR ALL TABLES");
+$node_subscriber->safe_psql('postgres',
+       "CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1"
+);
+
+# Wait for initial sync of all subscriptions
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+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');
+
+# data to replicate
+
+$node_publisher->safe_psql('postgres',
+       "INSERT INTO tab1 VALUES (4), (5)");
+
+$node_publisher->safe_psql('postgres',
+       "UPDATE tab1 SET a = 6 WHERE a = 5");
+
+$node_publisher->wait_for_catchup('sub1');
+
+$result = $node_subscriber->safe_psql('postgres',
+       "SELECT a, b FROM tab1");
+is($result, qq(1|22
+2|44
+3|66
+4|88
+6|132), 'generated columns replicated');