Generated columns
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
This commit is contained in:
parent
6b8b5364dd
commit
fc22b6623b
|
@ -6431,6 +6431,31 @@ select * from rem1;
|
||||||
11 | bye remote
|
11 | bye remote
|
||||||
(4 rows)
|
(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
|
-- test local triggers
|
||||||
-- ===================================================================
|
-- ===================================================================
|
||||||
|
|
|
@ -1644,9 +1644,10 @@ postgresPlanForeignModify(PlannerInfo *root,
|
||||||
else if (operation == CMD_UPDATE)
|
else if (operation == CMD_UPDATE)
|
||||||
{
|
{
|
||||||
int col;
|
int col;
|
||||||
|
Bitmapset *allUpdatedCols = bms_union(rte->updatedCols, rte->extraUpdatedCols);
|
||||||
|
|
||||||
col = -1;
|
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 */
|
/* bit numbers are offset by FirstLowInvalidHeapAttributeNumber */
|
||||||
AttrNumber attno = col + FirstLowInvalidHeapAttributeNumber;
|
AttrNumber attno = col + FirstLowInvalidHeapAttributeNumber;
|
||||||
|
|
|
@ -1363,6 +1363,20 @@ insert into rem1(f2) values('bye remote');
|
||||||
select * from loc1;
|
select * from loc1;
|
||||||
select * from rem1;
|
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
|
-- test local triggers
|
||||||
-- ===================================================================
|
-- ===================================================================
|
||||||
|
|
|
@ -1129,9 +1129,11 @@
|
||||||
<entry><type>bool</type></entry>
|
<entry><type>bool</type></entry>
|
||||||
<entry></entry>
|
<entry></entry>
|
||||||
<entry>
|
<entry>
|
||||||
This column has a default value, in which case there will be a
|
This column has a default expression or generation expression, in which
|
||||||
corresponding entry in the <structname>pg_attrdef</structname>
|
case there will be a corresponding entry in the
|
||||||
catalog that actually defines the value.
|
<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>
|
</entry>
|
||||||
</row>
|
</row>
|
||||||
|
|
||||||
|
@ -1159,6 +1161,17 @@
|
||||||
</entry>
|
</entry>
|
||||||
</row>
|
</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>
|
<row>
|
||||||
<entry><structfield>attisdropped</structfield></entry>
|
<entry><structfield>attisdropped</structfield></entry>
|
||||||
<entry><type>bool</type></entry>
|
<entry><type>bool</type></entry>
|
||||||
|
|
|
@ -233,6 +233,124 @@ CREATE TABLE products (
|
||||||
</para>
|
</para>
|
||||||
</sect1>
|
</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">
|
<sect1 id="ddl-constraints">
|
||||||
<title>Constraints</title>
|
<title>Constraints</title>
|
||||||
|
|
||||||
|
|
|
@ -952,6 +952,62 @@
|
||||||
</table>
|
</table>
|
||||||
</sect1>
|
</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">
|
<sect1 id="infoschema-column-domain-usage">
|
||||||
<title><literal>column_domain_usage</literal></title>
|
<title><literal>column_domain_usage</literal></title>
|
||||||
|
|
||||||
|
@ -1648,13 +1704,19 @@
|
||||||
<row>
|
<row>
|
||||||
<entry><literal>is_generated</literal></entry>
|
<entry><literal>is_generated</literal></entry>
|
||||||
<entry><type>character_data</type></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>
|
||||||
|
|
||||||
<row>
|
<row>
|
||||||
<entry><literal>generation_expression</literal></entry>
|
<entry><literal>generation_expression</literal></entry>
|
||||||
<entry><type>character_data</type></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>
|
||||||
|
|
||||||
<row>
|
<row>
|
||||||
|
|
|
@ -6450,7 +6450,7 @@ Relation
|
||||||
</listitem>
|
</listitem>
|
||||||
</varlistentry>
|
</varlistentry>
|
||||||
</variablelist>
|
</variablelist>
|
||||||
Next, the following message part appears for each column:
|
Next, the following message part appears for each column (except generated columns):
|
||||||
<variablelist>
|
<variablelist>
|
||||||
<varlistentry>
|
<varlistentry>
|
||||||
<term>
|
<term>
|
||||||
|
@ -6875,7 +6875,7 @@ TupleData
|
||||||
</listitem>
|
</listitem>
|
||||||
</varlistentry>
|
</varlistentry>
|
||||||
</variablelist>
|
</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>
|
<variablelist>
|
||||||
<varlistentry>
|
<varlistentry>
|
||||||
<term>
|
<term>
|
||||||
|
|
|
@ -103,7 +103,8 @@ COPY { <replaceable class="parameter">table_name</replaceable> [ ( <replaceable
|
||||||
<listitem>
|
<listitem>
|
||||||
<para>
|
<para>
|
||||||
An optional list of columns to be copied. If no column list is
|
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>
|
</para>
|
||||||
</listitem>
|
</listitem>
|
||||||
</varlistentry>
|
</varlistentry>
|
||||||
|
|
|
@ -42,7 +42,8 @@ CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name
|
||||||
{ NOT NULL |
|
{ NOT NULL |
|
||||||
NULL |
|
NULL |
|
||||||
CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
|
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>
|
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
|
||||||
|
|
||||||
|
@ -258,6 +259,30 @@ CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ]
|
||||||
</listitem>
|
</listitem>
|
||||||
</varlistentry>
|
</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>
|
<varlistentry>
|
||||||
<term><replaceable class="parameter">server_name</replaceable></term>
|
<term><replaceable class="parameter">server_name</replaceable></term>
|
||||||
<listitem>
|
<listitem>
|
||||||
|
|
|
@ -62,6 +62,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
|
||||||
NULL |
|
NULL |
|
||||||
CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
|
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 |
|
||||||
GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( <replaceable>sequence_options</replaceable> ) ] |
|
GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( <replaceable>sequence_options</replaceable> ) ] |
|
||||||
UNIQUE <replaceable class="parameter">index_parameters</replaceable> |
|
UNIQUE <replaceable class="parameter">index_parameters</replaceable> |
|
||||||
PRIMARY KEY <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>
|
<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>
|
<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>
|
</listitem>
|
||||||
</varlistentry>
|
</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>
|
<varlistentry>
|
||||||
<term><literal>INCLUDING IDENTITY</literal></term>
|
<term><literal>INCLUDING IDENTITY</literal></term>
|
||||||
<listitem>
|
<listitem>
|
||||||
|
@ -797,6 +808,28 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
|
||||||
</listitem>
|
</listitem>
|
||||||
</varlistentry>
|
</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>
|
<varlistentry>
|
||||||
<term><literal>GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( <replaceable>sequence_options</replaceable> ) ]</literal></term>
|
<term><literal>GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( <replaceable>sequence_options</replaceable> ) ]</literal></term>
|
||||||
<listitem>
|
<listitem>
|
||||||
|
@ -2028,6 +2061,16 @@ CREATE TABLE cities_partdef
|
||||||
</para>
|
</para>
|
||||||
</refsect2>
|
</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>
|
<refsect2>
|
||||||
<title><literal>LIKE</literal> Clause</title>
|
<title><literal>LIKE</literal> Clause</title>
|
||||||
|
|
||||||
|
|
|
@ -261,7 +261,9 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="parameter">name</replaceable>
|
||||||
UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</replaceable> ... ]
|
UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</replaceable> ... ]
|
||||||
</synopsis>
|
</synopsis>
|
||||||
The trigger will only fire if at least one of the listed columns
|
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>
|
||||||
|
|
||||||
<para>
|
<para>
|
||||||
|
|
|
@ -620,15 +620,17 @@ CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' |
|
||||||
|
|
||||||
<para>
|
<para>
|
||||||
Another approach is to create a separate <type>tsvector</type> column
|
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>,
|
concatenation of <literal>title</literal> and <literal>body</literal>,
|
||||||
using <function>coalesce</function> to ensure that one field will still be
|
using <function>coalesce</function> to ensure that one field will still be
|
||||||
indexed when the other is <literal>NULL</literal>:
|
indexed when the other is <literal>NULL</literal>:
|
||||||
|
|
||||||
<programlisting>
|
<programlisting>
|
||||||
ALTER TABLE pgweb ADD COLUMN textsearchable_index_col tsvector;
|
ALTER TABLE pgweb
|
||||||
UPDATE pgweb SET textsearchable_index_col =
|
ADD COLUMN textsearchable_index_col tsvector
|
||||||
to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,''));
|
GENERATED ALWAYS AS (to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))) STORED;
|
||||||
</programlisting>
|
</programlisting>
|
||||||
|
|
||||||
Then we create a <acronym>GIN</acronym> index to speed up the search:
|
Then we create a <acronym>GIN</acronym> index to speed up the search:
|
||||||
|
@ -648,14 +650,6 @@ LIMIT 10;
|
||||||
</programlisting>
|
</programlisting>
|
||||||
</para>
|
</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>
|
<para>
|
||||||
One advantage of the separate-column approach over an expression index
|
One advantage of the separate-column approach over an expression index
|
||||||
is that it is not necessary to explicitly specify the text search
|
is that it is not necessary to explicitly specify the text search
|
||||||
|
@ -1857,6 +1851,14 @@ SELECT ts_rewrite('a & b'::tsquery,
|
||||||
<secondary>for updating a derived tsvector column</secondary>
|
<secondary>for updating a derived tsvector column</secondary>
|
||||||
</indexterm>
|
</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>
|
<para>
|
||||||
When using a separate column to store the <type>tsvector</type> representation
|
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
|
of your documents, it is necessary to create a trigger to update the
|
||||||
|
|
|
@ -243,6 +243,24 @@
|
||||||
operation, and so they can return <symbol>NULL</symbol>.
|
operation, and so they can return <symbol>NULL</symbol>.
|
||||||
</para>
|
</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>
|
<para>
|
||||||
If more than one trigger is defined for the same event on the same
|
If more than one trigger is defined for the same event on the same
|
||||||
relation, the triggers will be fired in alphabetical order by
|
relation, the triggers will be fired in alphabetical order by
|
||||||
|
|
|
@ -131,6 +131,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
|
||||||
att->atthasdef = false;
|
att->atthasdef = false;
|
||||||
att->atthasmissing = false;
|
att->atthasmissing = false;
|
||||||
att->attidentity = '\0';
|
att->attidentity = '\0';
|
||||||
|
att->attgenerated = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* We can copy the tuple type identification, too */
|
/* We can copy the tuple type identification, too */
|
||||||
|
@ -165,6 +166,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
|
||||||
TupleConstr *cpy = (TupleConstr *) palloc0(sizeof(TupleConstr));
|
TupleConstr *cpy = (TupleConstr *) palloc0(sizeof(TupleConstr));
|
||||||
|
|
||||||
cpy->has_not_null = constr->has_not_null;
|
cpy->has_not_null = constr->has_not_null;
|
||||||
|
cpy->has_generated_stored = constr->has_generated_stored;
|
||||||
|
|
||||||
if ((cpy->num_defval = constr->num_defval) > 0)
|
if ((cpy->num_defval = constr->num_defval) > 0)
|
||||||
{
|
{
|
||||||
|
@ -247,6 +249,7 @@ TupleDescCopy(TupleDesc dst, TupleDesc src)
|
||||||
att->atthasdef = false;
|
att->atthasdef = false;
|
||||||
att->atthasmissing = false;
|
att->atthasmissing = false;
|
||||||
att->attidentity = '\0';
|
att->attidentity = '\0';
|
||||||
|
att->attgenerated = '\0';
|
||||||
}
|
}
|
||||||
dst->constr = NULL;
|
dst->constr = NULL;
|
||||||
|
|
||||||
|
@ -300,6 +303,7 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
|
||||||
dstAtt->atthasdef = false;
|
dstAtt->atthasdef = false;
|
||||||
dstAtt->atthasmissing = false;
|
dstAtt->atthasmissing = false;
|
||||||
dstAtt->attidentity = '\0';
|
dstAtt->attidentity = '\0';
|
||||||
|
dstAtt->attgenerated = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -456,6 +460,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
|
||||||
return false;
|
return false;
|
||||||
if (attr1->attidentity != attr2->attidentity)
|
if (attr1->attidentity != attr2->attidentity)
|
||||||
return false;
|
return false;
|
||||||
|
if (attr1->attgenerated != attr2->attgenerated)
|
||||||
|
return false;
|
||||||
if (attr1->attisdropped != attr2->attisdropped)
|
if (attr1->attisdropped != attr2->attisdropped)
|
||||||
return false;
|
return false;
|
||||||
if (attr1->attislocal != attr2->attislocal)
|
if (attr1->attislocal != attr2->attislocal)
|
||||||
|
@ -476,6 +482,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
|
||||||
return false;
|
return false;
|
||||||
if (constr1->has_not_null != constr2->has_not_null)
|
if (constr1->has_not_null != constr2->has_not_null)
|
||||||
return false;
|
return false;
|
||||||
|
if (constr1->has_generated_stored != constr2->has_generated_stored)
|
||||||
|
return false;
|
||||||
n = constr1->num_defval;
|
n = constr1->num_defval;
|
||||||
if (n != (int) constr2->num_defval)
|
if (n != (int) constr2->num_defval)
|
||||||
return false;
|
return false;
|
||||||
|
@ -638,6 +646,7 @@ TupleDescInitEntry(TupleDesc desc,
|
||||||
att->atthasdef = false;
|
att->atthasdef = false;
|
||||||
att->atthasmissing = false;
|
att->atthasmissing = false;
|
||||||
att->attidentity = '\0';
|
att->attidentity = '\0';
|
||||||
|
att->attgenerated = '\0';
|
||||||
att->attisdropped = false;
|
att->attisdropped = false;
|
||||||
att->attislocal = true;
|
att->attislocal = true;
|
||||||
att->attinhcount = 0;
|
att->attinhcount = 0;
|
||||||
|
@ -697,6 +706,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
|
||||||
att->atthasdef = false;
|
att->atthasdef = false;
|
||||||
att->atthasmissing = false;
|
att->atthasmissing = false;
|
||||||
att->attidentity = '\0';
|
att->attidentity = '\0';
|
||||||
|
att->attgenerated = '\0';
|
||||||
att->attisdropped = false;
|
att->attisdropped = false;
|
||||||
att->attislocal = true;
|
att->attislocal = true;
|
||||||
att->attinhcount = 0;
|
att->attinhcount = 0;
|
||||||
|
@ -853,6 +863,7 @@ BuildDescForRelation(List *schema)
|
||||||
TupleConstr *constr = (TupleConstr *) palloc0(sizeof(TupleConstr));
|
TupleConstr *constr = (TupleConstr *) palloc0(sizeof(TupleConstr));
|
||||||
|
|
||||||
constr->has_not_null = true;
|
constr->has_not_null = true;
|
||||||
|
constr->has_generated_stored = false;
|
||||||
constr->defval = NULL;
|
constr->defval = NULL;
|
||||||
constr->missing = NULL;
|
constr->missing = NULL;
|
||||||
constr->num_defval = 0;
|
constr->num_defval = 0;
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
#include "parser/parse_collate.h"
|
#include "parser/parse_collate.h"
|
||||||
#include "parser/parse_expr.h"
|
#include "parser/parse_expr.h"
|
||||||
#include "parser/parse_relation.h"
|
#include "parser/parse_relation.h"
|
||||||
|
#include "parser/parsetree.h"
|
||||||
#include "partitioning/partdesc.h"
|
#include "partitioning/partdesc.h"
|
||||||
#include "storage/lmgr.h"
|
#include "storage/lmgr.h"
|
||||||
#include "storage/predicate.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_atthasdef - 1] = BoolGetDatum(new_attribute->atthasdef);
|
||||||
values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(new_attribute->atthasmissing);
|
values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(new_attribute->atthasmissing);
|
||||||
values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(new_attribute->attidentity);
|
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_attisdropped - 1] = BoolGetDatum(new_attribute->attisdropped);
|
||||||
values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(new_attribute->attislocal);
|
values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(new_attribute->attislocal);
|
||||||
values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(new_attribute->attinhcount);
|
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 */
|
/* We don't want to keep stats for it anymore */
|
||||||
attStruct->attstattarget = 0;
|
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
|
* Change the column name to something that isn't likely to conflict
|
||||||
*/
|
*/
|
||||||
|
@ -2130,6 +2135,7 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
|
||||||
Relation attrrel;
|
Relation attrrel;
|
||||||
HeapTuple atttup;
|
HeapTuple atttup;
|
||||||
Form_pg_attribute attStruct;
|
Form_pg_attribute attStruct;
|
||||||
|
char attgenerated;
|
||||||
Oid attrdefOid;
|
Oid attrdefOid;
|
||||||
ObjectAddress colobject,
|
ObjectAddress colobject,
|
||||||
defobject;
|
defobject;
|
||||||
|
@ -2177,6 +2183,7 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
|
||||||
elog(ERROR, "cache lookup failed for attribute %d of relation %u",
|
elog(ERROR, "cache lookup failed for attribute %d of relation %u",
|
||||||
attnum, RelationGetRelid(rel));
|
attnum, RelationGetRelid(rel));
|
||||||
attStruct = (Form_pg_attribute) GETSTRUCT(atttup);
|
attStruct = (Form_pg_attribute) GETSTRUCT(atttup);
|
||||||
|
attgenerated = attStruct->attgenerated;
|
||||||
if (!attStruct->atthasdef)
|
if (!attStruct->atthasdef)
|
||||||
{
|
{
|
||||||
Form_pg_attribute defAttStruct;
|
Form_pg_attribute defAttStruct;
|
||||||
|
@ -2197,7 +2204,7 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
|
||||||
valuesAtt[Anum_pg_attribute_atthasdef - 1] = true;
|
valuesAtt[Anum_pg_attribute_atthasdef - 1] = true;
|
||||||
replacesAtt[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);
|
expr2 = expression_planner(expr2);
|
||||||
estate = CreateExecutorState();
|
estate = CreateExecutorState();
|
||||||
|
@ -2259,7 +2266,26 @@ StoreAttrDefault(Relation rel, AttrNumber attnum,
|
||||||
/*
|
/*
|
||||||
* Record dependencies on objects used in the expression, too.
|
* 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.
|
* Post creation hook for attribute defaults.
|
||||||
|
@ -2517,12 +2543,14 @@ AddRelationNewConstraints(Relation rel,
|
||||||
|
|
||||||
expr = cookDefault(pstate, colDef->raw_default,
|
expr = cookDefault(pstate, colDef->raw_default,
|
||||||
atp->atttypid, atp->atttypmod,
|
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
|
* If the expression is just a NULL constant, we do not bother to make
|
||||||
* an explicit pg_attrdef entry, since the default behavior is
|
* 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
|
* 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
|
* 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.
|
* override any default that the domain might have.
|
||||||
*/
|
*/
|
||||||
if (expr == NULL ||
|
if (expr == NULL ||
|
||||||
(IsA(expr, Const) &&((Const *) expr)->constisnull))
|
(!colDef->generated &&
|
||||||
|
IsA(expr, Const) &&
|
||||||
|
castNode(Const, expr)->constisnull))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
/* If the DEFAULT is volatile we cannot use a missing value */
|
/* If the DEFAULT is volatile we cannot use a missing value */
|
||||||
|
@ -2888,6 +2918,46 @@ SetRelationNumChecks(Relation rel, int numchecks)
|
||||||
table_close(relrel, RowExclusiveLock);
|
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
|
* Take a raw default and convert it to a cooked format ready for
|
||||||
* storage.
|
* storage.
|
||||||
|
@ -2905,7 +2975,8 @@ cookDefault(ParseState *pstate,
|
||||||
Node *raw_default,
|
Node *raw_default,
|
||||||
Oid atttypid,
|
Oid atttypid,
|
||||||
int32 atttypmod,
|
int32 atttypmod,
|
||||||
const char *attname)
|
const char *attname,
|
||||||
|
char attgenerated)
|
||||||
{
|
{
|
||||||
Node *expr;
|
Node *expr;
|
||||||
|
|
||||||
|
@ -2914,14 +2985,25 @@ cookDefault(ParseState *pstate,
|
||||||
/*
|
/*
|
||||||
* Transform raw parsetree to executable expression.
|
* 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);
|
||||||
|
|
||||||
/*
|
if (attgenerated)
|
||||||
* transformExpr() should have already rejected column references,
|
{
|
||||||
* subqueries, aggregates, window functions, and SRFs, based on the
|
check_nested_generated(pstate, expr);
|
||||||
* EXPR_KIND_ for a default expression.
|
|
||||||
*/
|
if (contain_mutable_functions(expr))
|
||||||
Assert(!contain_var_clause(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
|
* Coerce the expression to the correct type and typmod, if given. This
|
||||||
|
|
|
@ -509,7 +509,29 @@ GRANT SELECT ON collation_character_set_applicability TO PUBLIC;
|
||||||
* COLUMN_COLUMN_USAGE view
|
* 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(c.relname AS sql_identifier) AS table_name,
|
||||||
CAST(a.attname AS sql_identifier) AS column_name,
|
CAST(a.attname AS sql_identifier) AS column_name,
|
||||||
CAST(a.attnum AS cardinal_number) AS ordinal_position,
|
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
|
CAST(CASE WHEN a.attnotnull OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END
|
||||||
AS yes_or_no)
|
AS yes_or_no)
|
||||||
AS is_nullable,
|
AS is_nullable,
|
||||||
|
@ -745,8 +767,8 @@ CREATE VIEW columns AS
|
||||||
CAST(seq.seqmin AS character_data) AS identity_minimum,
|
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(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(CASE WHEN a.attgenerated <> '' THEN 'ALWAYS' ELSE 'NEVER' END AS character_data) AS is_generated,
|
||||||
CAST(null AS character_data) AS generation_expression,
|
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
|
CAST(CASE WHEN c.relkind IN ('r', 'p') OR
|
||||||
(c.relkind IN ('v', 'f') AND
|
(c.relkind IN ('v', 'f') AND
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
#include "commands/trigger.h"
|
#include "commands/trigger.h"
|
||||||
#include "executor/execPartition.h"
|
#include "executor/execPartition.h"
|
||||||
#include "executor/executor.h"
|
#include "executor/executor.h"
|
||||||
|
#include "executor/nodeModifyTable.h"
|
||||||
#include "executor/tuptable.h"
|
#include "executor/tuptable.h"
|
||||||
#include "foreign/fdwapi.h"
|
#include "foreign/fdwapi.h"
|
||||||
#include "libpq/libpq.h"
|
#include "libpq/libpq.h"
|
||||||
|
@ -2922,6 +2923,21 @@ CopyFrom(CopyState cstate)
|
||||||
}
|
}
|
||||||
else
|
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
|
* If the target is a plain table, check the constraints of
|
||||||
* the tuple.
|
* the tuple.
|
||||||
|
@ -3271,7 +3287,7 @@ BeginCopyFrom(ParseState *pstate,
|
||||||
fmgr_info(in_func_oid, &in_functions[attnum - 1]);
|
fmgr_info(in_func_oid, &in_functions[attnum - 1]);
|
||||||
|
|
||||||
/* Get default info if needed */
|
/* 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 */
|
/* attribute is NOT to be copied from input */
|
||||||
/* use default value if one exists */
|
/* 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
|
* or NIL if there was none (in which case we want all the non-dropped
|
||||||
* columns).
|
* 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.
|
* rel can be NULL ... it's only used for error reports.
|
||||||
*/
|
*/
|
||||||
static List *
|
static List *
|
||||||
|
@ -4893,6 +4914,8 @@ CopyGetAttnums(TupleDesc tupDesc, Relation rel, List *attnamelist)
|
||||||
{
|
{
|
||||||
if (TupleDescAttr(tupDesc, i)->attisdropped)
|
if (TupleDescAttr(tupDesc, i)->attisdropped)
|
||||||
continue;
|
continue;
|
||||||
|
if (TupleDescAttr(tupDesc, i)->attgenerated)
|
||||||
|
continue;
|
||||||
attnums = lappend_int(attnums, i + 1);
|
attnums = lappend_int(attnums, i + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4917,6 +4940,12 @@ CopyGetAttnums(TupleDesc tupDesc, Relation rel, List *attnamelist)
|
||||||
continue;
|
continue;
|
||||||
if (namestrcmp(&(att->attname), name) == 0)
|
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;
|
attnum = att->attnum;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -760,6 +760,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
|
||||||
rawEnt->attnum = attnum;
|
rawEnt->attnum = attnum;
|
||||||
rawEnt->raw_default = colDef->raw_default;
|
rawEnt->raw_default = colDef->raw_default;
|
||||||
rawEnt->missingMode = false;
|
rawEnt->missingMode = false;
|
||||||
|
rawEnt->generated = colDef->generated;
|
||||||
rawDefaults = lappend(rawDefaults, rawEnt);
|
rawDefaults = lappend(rawDefaults, rawEnt);
|
||||||
attr->atthasdef = true;
|
attr->atthasdef = true;
|
||||||
}
|
}
|
||||||
|
@ -783,6 +784,9 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
|
||||||
|
|
||||||
if (colDef->identity)
|
if (colDef->identity)
|
||||||
attr->attidentity = 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);
|
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. */
|
/* Process and store partition bound, if any. */
|
||||||
if (stmt->partbound)
|
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
|
* Now add any newly specified CHECK constraints to the new relation.
|
||||||
* to the new relation. These are passed to us in the form of raw
|
* Same as for defaults above, but these need to come after partitioning
|
||||||
* parsetrees; we need to transform them to executable expression trees
|
* is set up.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
if (rawDefaults || stmt->constraints)
|
if (stmt->constraints)
|
||||||
AddRelationNewConstraints(rel, rawDefaults, stmt->constraints,
|
AddRelationNewConstraints(rel, NIL, stmt->constraints,
|
||||||
true, true, false, queryString);
|
true, true, false, queryString);
|
||||||
|
|
||||||
ObjectAddressSet(address, RelationRelationId, relationId);
|
ObjectAddressSet(address, RelationRelationId, relationId);
|
||||||
|
@ -2252,6 +2273,13 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
|
||||||
def->is_not_null |= attribute->attnotnull;
|
def->is_not_null |= attribute->attnotnull;
|
||||||
/* Default and other constraints are handled below */
|
/* Default and other constraints are handled below */
|
||||||
newattno[parent_attno - 1] = exist_attno;
|
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
|
else
|
||||||
{
|
{
|
||||||
|
@ -2269,6 +2297,7 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
|
||||||
def->storage = attribute->attstorage;
|
def->storage = attribute->attstorage;
|
||||||
def->raw_default = NULL;
|
def->raw_default = NULL;
|
||||||
def->cooked_default = NULL;
|
def->cooked_default = NULL;
|
||||||
|
def->generated = attribute->attgenerated;
|
||||||
def->collClause = NULL;
|
def->collClause = NULL;
|
||||||
def->collOid = attribute->attcollation;
|
def->collOid = attribute->attcollation;
|
||||||
def->constraints = NIL;
|
def->constraints = NIL;
|
||||||
|
@ -5613,6 +5642,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
|
||||||
attribute.atthasdef = false;
|
attribute.atthasdef = false;
|
||||||
attribute.atthasmissing = false;
|
attribute.atthasmissing = false;
|
||||||
attribute.attidentity = colDef->identity;
|
attribute.attidentity = colDef->identity;
|
||||||
|
attribute.attgenerated = colDef->generated;
|
||||||
attribute.attisdropped = false;
|
attribute.attisdropped = false;
|
||||||
attribute.attislocal = colDef->is_local;
|
attribute.attislocal = colDef->is_local;
|
||||||
attribute.attinhcount = colDef->inhcount;
|
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
|
* DEFAULT value outside of the heap. This may be disabled inside
|
||||||
* AddRelationNewConstraints if the optimization cannot be applied.
|
* 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
|
* This function is intended for CREATE TABLE, so it processes a
|
||||||
|
@ -6239,6 +6271,12 @@ ATExecColumnDefault(Relation rel, const char *colName,
|
||||||
colName, RelationGetRelationName(rel)),
|
colName, RelationGetRelationName(rel)),
|
||||||
newDefault ? 0 : errhint("Use ALTER TABLE ... ALTER COLUMN ... DROP IDENTITY instead.")));
|
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
|
* 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
|
* 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->attnum = attnum;
|
||||||
rawEnt->raw_default = newDefault;
|
rawEnt->raw_default = newDefault;
|
||||||
rawEnt->missingMode = false;
|
rawEnt->missingMode = false;
|
||||||
|
rawEnt->generated = '\0';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This function is intended for CREATE TABLE, so it processes a
|
* 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);
|
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.
|
* Look up the equality operators to use in the constraint.
|
||||||
*
|
*
|
||||||
|
@ -9951,10 +10016,18 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
|
||||||
COERCE_IMPLICIT_CAST,
|
COERCE_IMPLICIT_CAST,
|
||||||
-1);
|
-1);
|
||||||
if (defaultexpr == NULL)
|
if (defaultexpr == NULL)
|
||||||
ereport(ERROR,
|
{
|
||||||
(errcode(ERRCODE_DATATYPE_MISMATCH),
|
if (attTup->attgenerated)
|
||||||
errmsg("default for column \"%s\" cannot be cast automatically to type %s",
|
ereport(ERROR,
|
||||||
colName, format_type_be(targettype))));
|
(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
|
else
|
||||||
defaultexpr = NULL;
|
defaultexpr = NULL;
|
||||||
|
@ -10030,6 +10103,21 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
|
||||||
*/
|
*/
|
||||||
Assert(foundObject.objectSubId == 0);
|
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
|
else
|
||||||
{
|
{
|
||||||
/* Not expecting any other direct dependencies... */
|
/* 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
|
* 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
|
* 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],
|
ScanKeyInit(&key[0],
|
||||||
Anum_pg_depend_classid,
|
Anum_pg_depend_classid,
|
||||||
|
@ -10195,15 +10284,26 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
|
||||||
while (HeapTupleIsValid(depTup = systable_getnext(scan)))
|
while (HeapTupleIsValid(depTup = systable_getnext(scan)))
|
||||||
{
|
{
|
||||||
Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(depTup);
|
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'",
|
elog(ERROR, "found unexpected dependency type '%c'",
|
||||||
foundDep->deptype);
|
foundDep->deptype);
|
||||||
if (!(foundDep->refclassid == TypeRelationId &&
|
if (!(foundDep->refclassid == TypeRelationId &&
|
||||||
foundDep->refobjid == attTup->atttypid) &&
|
foundDep->refobjid == attTup->atttypid) &&
|
||||||
!(foundDep->refclassid == CollationRelationId &&
|
!(foundDep->refclassid == CollationRelationId &&
|
||||||
foundDep->refobjid == attTup->attcollation))
|
foundDep->refobjid == attTup->attcollation) &&
|
||||||
elog(ERROR, "found unexpected dependency for column");
|
!(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);
|
CatalogTupleDelete(depRel, &depTup->t_self);
|
||||||
}
|
}
|
||||||
|
@ -14267,6 +14367,18 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
|
||||||
pelem->name),
|
pelem->name),
|
||||||
parser_errposition(pstate, pelem->location)));
|
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;
|
partattrs[attn] = attform->attnum;
|
||||||
atttype = attform->atttypid;
|
atttype = attform->atttypid;
|
||||||
attcollation = attform->attcollation;
|
attcollation = attform->attcollation;
|
||||||
|
@ -14354,6 +14466,25 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
|
||||||
errmsg("partition key expressions cannot contain system column references")));
|
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
|
* While it is not exactly *wrong* for a partition expression
|
||||||
* to be a constant, it seems better to reject such keys.
|
* to be a constant, it seems better to reject such keys.
|
||||||
|
|
|
@ -75,8 +75,9 @@ static int MyTriggerDepth = 0;
|
||||||
* they use, so we let them be duplicated. Be sure to update all if one needs
|
* they use, so we let them be duplicated. Be sure to update all if one needs
|
||||||
* to be changed, however.
|
* to be changed, however.
|
||||||
*/
|
*/
|
||||||
#define GetUpdatedColumns(relinfo, estate) \
|
#define GetAllUpdatedColumns(relinfo, estate) \
|
||||||
(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols)
|
(bms_union(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols, \
|
||||||
|
exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->extraUpdatedCols))
|
||||||
|
|
||||||
/* Local function prototypes */
|
/* Local function prototypes */
|
||||||
static void ConvertTriggerToFK(CreateTrigStmt *stmt, Oid funcoid);
|
static void ConvertTriggerToFK(CreateTrigStmt *stmt, Oid funcoid);
|
||||||
|
@ -640,6 +641,24 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
|
||||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||||
errmsg("BEFORE trigger's WHEN condition cannot reference NEW system columns"),
|
errmsg("BEFORE trigger's WHEN condition cannot reference NEW system columns"),
|
||||||
parser_errposition(pstate, var->location)));
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
/* can't happen without add_missing_from, so just elog */
|
/* can't happen without add_missing_from, so just elog */
|
||||||
|
@ -2931,7 +2950,7 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
|
||||||
CMD_UPDATE))
|
CMD_UPDATE))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
updatedCols = GetUpdatedColumns(relinfo, estate);
|
updatedCols = GetAllUpdatedColumns(relinfo, estate);
|
||||||
|
|
||||||
LocTriggerData.type = T_TriggerData;
|
LocTriggerData.type = T_TriggerData;
|
||||||
LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
|
LocTriggerData.tg_event = TRIGGER_EVENT_UPDATE |
|
||||||
|
@ -2980,7 +2999,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
|
||||||
if (trigdesc && trigdesc->trig_update_after_statement)
|
if (trigdesc && trigdesc->trig_update_after_statement)
|
||||||
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
|
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
|
||||||
false, NULL, NULL, NIL,
|
false, NULL, NULL, NIL,
|
||||||
GetUpdatedColumns(relinfo, estate),
|
GetAllUpdatedColumns(relinfo, estate),
|
||||||
transition_capture);
|
transition_capture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3049,7 +3068,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
|
||||||
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
|
||||||
LocTriggerData.tg_oldtable = NULL;
|
LocTriggerData.tg_oldtable = NULL;
|
||||||
LocTriggerData.tg_newtable = NULL;
|
LocTriggerData.tg_newtable = NULL;
|
||||||
updatedCols = GetUpdatedColumns(relinfo, estate);
|
updatedCols = GetAllUpdatedColumns(relinfo, estate);
|
||||||
for (i = 0; i < trigdesc->numtriggers; i++)
|
for (i = 0; i < trigdesc->numtriggers; i++)
|
||||||
{
|
{
|
||||||
Trigger *trigger = &trigdesc->triggers[i];
|
Trigger *trigger = &trigdesc->triggers[i];
|
||||||
|
@ -3140,7 +3159,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
|
||||||
|
|
||||||
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
|
AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
|
||||||
true, oldslot, newslot, recheckIndexes,
|
true, oldslot, newslot, recheckIndexes,
|
||||||
GetUpdatedColumns(relinfo, estate),
|
GetAllUpdatedColumns(relinfo, estate),
|
||||||
transition_capture);
|
transition_capture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -918,7 +918,8 @@ DefineDomain(CreateDomainStmt *stmt)
|
||||||
defaultExpr = cookDefault(pstate, constr->raw_expr,
|
defaultExpr = cookDefault(pstate, constr->raw_expr,
|
||||||
basetypeoid,
|
basetypeoid,
|
||||||
basetypeMod,
|
basetypeMod,
|
||||||
domainName);
|
domainName,
|
||||||
|
0);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If the expression is just a NULL constant, we treat it
|
* If the expression is just a NULL constant, we treat it
|
||||||
|
@ -2228,7 +2229,8 @@ AlterDomainDefault(List *names, Node *defaultRaw)
|
||||||
defaultExpr = cookDefault(pstate, defaultRaw,
|
defaultExpr = cookDefault(pstate, defaultRaw,
|
||||||
typTup->typbasetype,
|
typTup->typbasetype,
|
||||||
typTup->typtypmod,
|
typTup->typtypmod,
|
||||||
NameStr(typTup->typname));
|
NameStr(typTup->typname),
|
||||||
|
0);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If the expression is just a NULL constant, we treat the command
|
* If the expression is just a NULL constant, we treat the command
|
||||||
|
|
|
@ -102,7 +102,7 @@ static void EvalPlanQualStart(EPQState *epqstate, EState *parentestate,
|
||||||
Plan *planTree);
|
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
|
* 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
|
* it uses, so we let them be duplicated. Be sure to update both if one needs
|
||||||
* to be changed, however.
|
* to be changed, however.
|
||||||
|
@ -111,6 +111,9 @@ static void EvalPlanQualStart(EPQState *epqstate, EState *parentestate,
|
||||||
(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->insertedCols)
|
(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->insertedCols)
|
||||||
#define GetUpdatedColumns(relinfo, estate) \
|
#define GetUpdatedColumns(relinfo, estate) \
|
||||||
(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols)
|
(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 */
|
/* end of local decls */
|
||||||
|
|
||||||
|
@ -1316,6 +1319,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
|
||||||
resultRelInfo->ri_FdwState = NULL;
|
resultRelInfo->ri_FdwState = NULL;
|
||||||
resultRelInfo->ri_usesFdwDirectModify = false;
|
resultRelInfo->ri_usesFdwDirectModify = false;
|
||||||
resultRelInfo->ri_ConstraintExprs = NULL;
|
resultRelInfo->ri_ConstraintExprs = NULL;
|
||||||
|
resultRelInfo->ri_GeneratedExprs = NULL;
|
||||||
resultRelInfo->ri_junkFilter = NULL;
|
resultRelInfo->ri_junkFilter = NULL;
|
||||||
resultRelInfo->ri_projectReturning = NULL;
|
resultRelInfo->ri_projectReturning = NULL;
|
||||||
resultRelInfo->ri_onConflictArbiterIndexes = NIL;
|
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
|
* been modified, then we can use a weaker lock, allowing for better
|
||||||
* concurrency.
|
* concurrency.
|
||||||
*/
|
*/
|
||||||
updatedCols = GetUpdatedColumns(relinfo, estate);
|
updatedCols = GetAllUpdatedColumns(relinfo, estate);
|
||||||
keyCols = RelationGetIndexAttrBitmap(relinfo->ri_RelationDesc,
|
keyCols = RelationGetIndexAttrBitmap(relinfo->ri_RelationDesc,
|
||||||
INDEX_ATTR_BITMAP_KEY);
|
INDEX_ATTR_BITMAP_KEY);
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
#include "access/xact.h"
|
#include "access/xact.h"
|
||||||
#include "commands/trigger.h"
|
#include "commands/trigger.h"
|
||||||
#include "executor/executor.h"
|
#include "executor/executor.h"
|
||||||
|
#include "executor/nodeModifyTable.h"
|
||||||
#include "nodes/nodeFuncs.h"
|
#include "nodes/nodeFuncs.h"
|
||||||
#include "parser/parse_relation.h"
|
#include "parser/parse_relation.h"
|
||||||
#include "parser/parsetree.h"
|
#include "parser/parsetree.h"
|
||||||
|
@ -412,6 +413,11 @@ ExecSimpleRelationInsert(EState *estate, TupleTableSlot *slot)
|
||||||
{
|
{
|
||||||
List *recheckIndexes = NIL;
|
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 */
|
/* Check the constraints of the tuple */
|
||||||
if (rel->rd_att->constr)
|
if (rel->rd_att->constr)
|
||||||
ExecConstraints(resultRelInfo, slot, estate);
|
ExecConstraints(resultRelInfo, slot, estate);
|
||||||
|
@ -473,6 +479,11 @@ ExecSimpleRelationUpdate(EState *estate, EPQState *epqstate,
|
||||||
List *recheckIndexes = NIL;
|
List *recheckIndexes = NIL;
|
||||||
bool update_indexes;
|
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 */
|
/* Check the constraints of the tuple */
|
||||||
if (rel->rd_att->constr)
|
if (rel->rd_att->constr)
|
||||||
ExecConstraints(resultRelInfo, slot, estate);
|
ExecConstraints(resultRelInfo, slot, estate);
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
#include "foreign/fdwapi.h"
|
#include "foreign/fdwapi.h"
|
||||||
#include "miscadmin.h"
|
#include "miscadmin.h"
|
||||||
#include "nodes/nodeFuncs.h"
|
#include "nodes/nodeFuncs.h"
|
||||||
|
#include "rewrite/rewriteHandler.h"
|
||||||
#include "storage/bufmgr.h"
|
#include "storage/bufmgr.h"
|
||||||
#include "storage/lmgr.h"
|
#include "storage/lmgr.h"
|
||||||
#include "utils/builtins.h"
|
#include "utils/builtins.h"
|
||||||
|
@ -240,6 +241,89 @@ ExecCheckTIDVisible(EState *estate,
|
||||||
ExecClearTuple(tempSlot);
|
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
|
* ExecInsert
|
||||||
*
|
*
|
||||||
|
@ -297,6 +381,13 @@ ExecInsert(ModifyTableState *mtstate,
|
||||||
}
|
}
|
||||||
else if (resultRelInfo->ri_FdwRoutine)
|
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
|
* insert into foreign table: let the FDW do it
|
||||||
*/
|
*/
|
||||||
|
@ -326,6 +417,13 @@ ExecInsert(ModifyTableState *mtstate,
|
||||||
*/
|
*/
|
||||||
slot->tts_tableOid = RelationGetRelid(resultRelationDesc);
|
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.
|
* Check any RLS WITH CHECK policies.
|
||||||
*
|
*
|
||||||
|
@ -964,6 +1062,13 @@ ExecUpdate(ModifyTableState *mtstate,
|
||||||
}
|
}
|
||||||
else if (resultRelInfo->ri_FdwRoutine)
|
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
|
* update in foreign table: let the FDW do it
|
||||||
*/
|
*/
|
||||||
|
@ -994,6 +1099,13 @@ ExecUpdate(ModifyTableState *mtstate,
|
||||||
*/
|
*/
|
||||||
slot->tts_tableOid = RelationGetRelid(resultRelationDesc);
|
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
|
* Check any RLS UPDATE WITH CHECK policies
|
||||||
*
|
*
|
||||||
|
|
|
@ -2390,6 +2390,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
|
||||||
COPY_BITMAPSET_FIELD(selectedCols);
|
COPY_BITMAPSET_FIELD(selectedCols);
|
||||||
COPY_BITMAPSET_FIELD(insertedCols);
|
COPY_BITMAPSET_FIELD(insertedCols);
|
||||||
COPY_BITMAPSET_FIELD(updatedCols);
|
COPY_BITMAPSET_FIELD(updatedCols);
|
||||||
|
COPY_BITMAPSET_FIELD(extraUpdatedCols);
|
||||||
COPY_NODE_FIELD(securityQuals);
|
COPY_NODE_FIELD(securityQuals);
|
||||||
|
|
||||||
return newnode;
|
return newnode;
|
||||||
|
@ -2888,6 +2889,7 @@ _copyColumnDef(const ColumnDef *from)
|
||||||
COPY_NODE_FIELD(cooked_default);
|
COPY_NODE_FIELD(cooked_default);
|
||||||
COPY_SCALAR_FIELD(identity);
|
COPY_SCALAR_FIELD(identity);
|
||||||
COPY_NODE_FIELD(identitySequence);
|
COPY_NODE_FIELD(identitySequence);
|
||||||
|
COPY_SCALAR_FIELD(generated);
|
||||||
COPY_NODE_FIELD(collClause);
|
COPY_NODE_FIELD(collClause);
|
||||||
COPY_SCALAR_FIELD(collOid);
|
COPY_SCALAR_FIELD(collOid);
|
||||||
COPY_NODE_FIELD(constraints);
|
COPY_NODE_FIELD(constraints);
|
||||||
|
|
|
@ -2565,6 +2565,7 @@ _equalColumnDef(const ColumnDef *a, const ColumnDef *b)
|
||||||
COMPARE_NODE_FIELD(cooked_default);
|
COMPARE_NODE_FIELD(cooked_default);
|
||||||
COMPARE_SCALAR_FIELD(identity);
|
COMPARE_SCALAR_FIELD(identity);
|
||||||
COMPARE_NODE_FIELD(identitySequence);
|
COMPARE_NODE_FIELD(identitySequence);
|
||||||
|
COMPARE_SCALAR_FIELD(generated);
|
||||||
COMPARE_NODE_FIELD(collClause);
|
COMPARE_NODE_FIELD(collClause);
|
||||||
COMPARE_SCALAR_FIELD(collOid);
|
COMPARE_SCALAR_FIELD(collOid);
|
||||||
COMPARE_NODE_FIELD(constraints);
|
COMPARE_NODE_FIELD(constraints);
|
||||||
|
@ -2664,6 +2665,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
|
||||||
COMPARE_BITMAPSET_FIELD(selectedCols);
|
COMPARE_BITMAPSET_FIELD(selectedCols);
|
||||||
COMPARE_BITMAPSET_FIELD(insertedCols);
|
COMPARE_BITMAPSET_FIELD(insertedCols);
|
||||||
COMPARE_BITMAPSET_FIELD(updatedCols);
|
COMPARE_BITMAPSET_FIELD(updatedCols);
|
||||||
|
COMPARE_BITMAPSET_FIELD(extraUpdatedCols);
|
||||||
COMPARE_NODE_FIELD(securityQuals);
|
COMPARE_NODE_FIELD(securityQuals);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -2792,6 +2792,7 @@ _outColumnDef(StringInfo str, const ColumnDef *node)
|
||||||
WRITE_NODE_FIELD(cooked_default);
|
WRITE_NODE_FIELD(cooked_default);
|
||||||
WRITE_CHAR_FIELD(identity);
|
WRITE_CHAR_FIELD(identity);
|
||||||
WRITE_NODE_FIELD(identitySequence);
|
WRITE_NODE_FIELD(identitySequence);
|
||||||
|
WRITE_CHAR_FIELD(generated);
|
||||||
WRITE_NODE_FIELD(collClause);
|
WRITE_NODE_FIELD(collClause);
|
||||||
WRITE_OID_FIELD(collOid);
|
WRITE_OID_FIELD(collOid);
|
||||||
WRITE_NODE_FIELD(constraints);
|
WRITE_NODE_FIELD(constraints);
|
||||||
|
@ -3096,6 +3097,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
|
||||||
WRITE_BITMAPSET_FIELD(selectedCols);
|
WRITE_BITMAPSET_FIELD(selectedCols);
|
||||||
WRITE_BITMAPSET_FIELD(insertedCols);
|
WRITE_BITMAPSET_FIELD(insertedCols);
|
||||||
WRITE_BITMAPSET_FIELD(updatedCols);
|
WRITE_BITMAPSET_FIELD(updatedCols);
|
||||||
|
WRITE_BITMAPSET_FIELD(extraUpdatedCols);
|
||||||
WRITE_NODE_FIELD(securityQuals);
|
WRITE_NODE_FIELD(securityQuals);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3467,6 +3469,13 @@ _outConstraint(StringInfo str, const Constraint *node)
|
||||||
WRITE_CHAR_FIELD(generated_when);
|
WRITE_CHAR_FIELD(generated_when);
|
||||||
break;
|
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:
|
case CONSTR_CHECK:
|
||||||
appendStringInfoString(str, "CHECK");
|
appendStringInfoString(str, "CHECK");
|
||||||
WRITE_BOOL_FIELD(is_no_inherit);
|
WRITE_BOOL_FIELD(is_no_inherit);
|
||||||
|
|
|
@ -1430,6 +1430,7 @@ _readRangeTblEntry(void)
|
||||||
READ_BITMAPSET_FIELD(selectedCols);
|
READ_BITMAPSET_FIELD(selectedCols);
|
||||||
READ_BITMAPSET_FIELD(insertedCols);
|
READ_BITMAPSET_FIELD(insertedCols);
|
||||||
READ_BITMAPSET_FIELD(updatedCols);
|
READ_BITMAPSET_FIELD(updatedCols);
|
||||||
|
READ_BITMAPSET_FIELD(extraUpdatedCols);
|
||||||
READ_NODE_FIELD(securityQuals);
|
READ_NODE_FIELD(securityQuals);
|
||||||
|
|
||||||
READ_DONE();
|
READ_DONE();
|
||||||
|
|
|
@ -6570,8 +6570,9 @@ make_modifytable(PlannerInfo *root,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Try to modify the foreign table directly if (1) the FDW provides
|
* Try to modify the foreign table directly if (1) the FDW provides
|
||||||
* callback functions needed for that, (2) there are no row-level
|
* callback functions needed for that and (2) there are no local
|
||||||
* triggers on the foreign table, and (3) there are no WITH CHECK
|
* 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.
|
* OPTIONs from parent views.
|
||||||
*/
|
*/
|
||||||
direct_modify = false;
|
direct_modify = false;
|
||||||
|
@ -6581,7 +6582,8 @@ make_modifytable(PlannerInfo *root,
|
||||||
fdwroutine->IterateDirectModify != NULL &&
|
fdwroutine->IterateDirectModify != NULL &&
|
||||||
fdwroutine->EndDirectModify != NULL &&
|
fdwroutine->EndDirectModify != NULL &&
|
||||||
withCheckOptionLists == NIL &&
|
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);
|
direct_modify = fdwroutine->PlanDirectModify(subroot, node, rti, i);
|
||||||
if (direct_modify)
|
if (direct_modify)
|
||||||
direct_modify_plans = bms_add_member(direct_modify_plans, i);
|
direct_modify_plans = bms_add_member(direct_modify_plans, i);
|
||||||
|
|
|
@ -280,6 +280,10 @@ expand_partitioned_rtentry(PlannerInfo *root, RangeTblEntry *parentrte,
|
||||||
if (!root->partColsUpdated)
|
if (!root->partColsUpdated)
|
||||||
root->partColsUpdated =
|
root->partColsUpdated =
|
||||||
has_partition_attrs(parentrel, parentrte->updatedCols, NULL);
|
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
|
* 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);
|
appinfo->translated_vars);
|
||||||
childrte->updatedCols = translate_col_privs(parentrte->updatedCols,
|
childrte->updatedCols = translate_col_privs(parentrte->updatedCols,
|
||||||
appinfo->translated_vars);
|
appinfo->translated_vars);
|
||||||
|
childrte->extraUpdatedCols = translate_col_privs(parentrte->extraUpdatedCols,
|
||||||
|
appinfo->translated_vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -2083,6 +2083,25 @@ has_row_triggers(PlannerInfo *root, Index rti, CmdType event)
|
||||||
return result;
|
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
|
* set_relation_partition_info
|
||||||
*
|
*
|
||||||
|
|
|
@ -2287,6 +2287,7 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
|
||||||
RangeTblEntry *target_rte;
|
RangeTblEntry *target_rte;
|
||||||
ListCell *orig_tl;
|
ListCell *orig_tl;
|
||||||
ListCell *tl;
|
ListCell *tl;
|
||||||
|
TupleDesc tupdesc = pstate->p_target_relation->rd_att;
|
||||||
|
|
||||||
tlist = transformTargetList(pstate, origTlist,
|
tlist = transformTargetList(pstate, origTlist,
|
||||||
EXPR_KIND_UPDATE_SOURCE);
|
EXPR_KIND_UPDATE_SOURCE);
|
||||||
|
@ -2345,6 +2346,32 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
|
||||||
if (orig_tl != NULL)
|
if (orig_tl != NULL)
|
||||||
elog(ERROR, "UPDATE target count mismatch --- internal error");
|
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;
|
return tlist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -681,7 +681,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
|
||||||
SAVEPOINT SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES
|
SAVEPOINT SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES
|
||||||
SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
|
SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
|
||||||
SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P
|
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
|
SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P
|
||||||
|
|
||||||
TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN
|
TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN
|
||||||
|
@ -3497,6 +3497,16 @@ ColConstraintElem:
|
||||||
n->location = @1;
|
n->location = @1;
|
||||||
$$ = (Node *)n;
|
$$ = (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
|
| REFERENCES qualified_name opt_column_list key_match key_actions
|
||||||
{
|
{
|
||||||
Constraint *n = makeNode(Constraint);
|
Constraint *n = makeNode(Constraint);
|
||||||
|
@ -3587,6 +3597,7 @@ TableLikeOption:
|
||||||
| CONSTRAINTS { $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
|
| CONSTRAINTS { $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
|
||||||
| DEFAULTS { $$ = CREATE_TABLE_LIKE_DEFAULTS; }
|
| DEFAULTS { $$ = CREATE_TABLE_LIKE_DEFAULTS; }
|
||||||
| IDENTITY_P { $$ = CREATE_TABLE_LIKE_IDENTITY; }
|
| IDENTITY_P { $$ = CREATE_TABLE_LIKE_IDENTITY; }
|
||||||
|
| GENERATED { $$ = CREATE_TABLE_LIKE_GENERATED; }
|
||||||
| INDEXES { $$ = CREATE_TABLE_LIKE_INDEXES; }
|
| INDEXES { $$ = CREATE_TABLE_LIKE_INDEXES; }
|
||||||
| STATISTICS { $$ = CREATE_TABLE_LIKE_STATISTICS; }
|
| STATISTICS { $$ = CREATE_TABLE_LIKE_STATISTICS; }
|
||||||
| STORAGE { $$ = CREATE_TABLE_LIKE_STORAGE; }
|
| STORAGE { $$ = CREATE_TABLE_LIKE_STORAGE; }
|
||||||
|
@ -15234,6 +15245,7 @@ unreserved_keyword:
|
||||||
| STDIN
|
| STDIN
|
||||||
| STDOUT
|
| STDOUT
|
||||||
| STORAGE
|
| STORAGE
|
||||||
|
| STORED
|
||||||
| STRICT_P
|
| STRICT_P
|
||||||
| STRIP_P
|
| STRIP_P
|
||||||
| SUBSCRIPTION
|
| SUBSCRIPTION
|
||||||
|
|
|
@ -520,6 +520,14 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
|
||||||
err = _("grouping operations are not allowed in partition key expressions");
|
err = _("grouping operations are not allowed in partition key expressions");
|
||||||
|
|
||||||
break;
|
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:
|
case EXPR_KIND_CALL_ARGUMENT:
|
||||||
if (isAgg)
|
if (isAgg)
|
||||||
|
@ -922,6 +930,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
|
||||||
case EXPR_KIND_COPY_WHERE:
|
case EXPR_KIND_COPY_WHERE:
|
||||||
err = _("window functions are not allowed in COPY FROM WHERE conditions");
|
err = _("window functions are not allowed in COPY FROM WHERE conditions");
|
||||||
break;
|
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
|
* There is intentionally no default: case here, so that the
|
||||||
|
|
|
@ -570,6 +570,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
|
||||||
case EXPR_KIND_PARTITION_EXPRESSION:
|
case EXPR_KIND_PARTITION_EXPRESSION:
|
||||||
case EXPR_KIND_CALL_ARGUMENT:
|
case EXPR_KIND_CALL_ARGUMENT:
|
||||||
case EXPR_KIND_COPY_WHERE:
|
case EXPR_KIND_COPY_WHERE:
|
||||||
|
case EXPR_KIND_GENERATED_COLUMN:
|
||||||
/* okay */
|
/* okay */
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -1927,6 +1928,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
|
||||||
case EXPR_KIND_COPY_WHERE:
|
case EXPR_KIND_COPY_WHERE:
|
||||||
err = _("cannot use subquery in COPY FROM WHERE condition");
|
err = _("cannot use subquery in COPY FROM WHERE condition");
|
||||||
break;
|
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
|
* There is intentionally no default: case here, so that the
|
||||||
|
@ -3557,6 +3561,8 @@ ParseExprKindName(ParseExprKind exprKind)
|
||||||
return "CALL";
|
return "CALL";
|
||||||
case EXPR_KIND_COPY_WHERE:
|
case EXPR_KIND_COPY_WHERE:
|
||||||
return "WHERE";
|
return "WHERE";
|
||||||
|
case EXPR_KIND_GENERATED_COLUMN:
|
||||||
|
return "GENERATED AS";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* There is intentionally no default: case here, so that the
|
* There is intentionally no default: case here, so that the
|
||||||
|
|
|
@ -2526,6 +2526,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
|
||||||
case EXPR_KIND_COPY_WHERE:
|
case EXPR_KIND_COPY_WHERE:
|
||||||
err = _("set-returning functions are not allowed in COPY FROM WHERE conditions");
|
err = _("set-returning functions are not allowed in COPY FROM WHERE conditions");
|
||||||
break;
|
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
|
* There is intentionally no default: case here, so that the
|
||||||
|
|
|
@ -731,6 +731,17 @@ scanRTEForColumn(ParseState *pstate, RangeTblEntry *rte, const char *colname,
|
||||||
colname),
|
colname),
|
||||||
parser_errposition(pstate, location)));
|
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)
|
if (attnum != InvalidAttrNumber)
|
||||||
{
|
{
|
||||||
/* now check to see if column actually is defined */
|
/* now check to see if column actually is defined */
|
||||||
|
@ -1257,6 +1268,7 @@ addRangeTableEntry(ParseState *pstate,
|
||||||
rte->selectedCols = NULL;
|
rte->selectedCols = NULL;
|
||||||
rte->insertedCols = NULL;
|
rte->insertedCols = NULL;
|
||||||
rte->updatedCols = NULL;
|
rte->updatedCols = NULL;
|
||||||
|
rte->extraUpdatedCols = NULL;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add completed RTE to pstate's range table list, but not to join list
|
* 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->selectedCols = NULL;
|
||||||
rte->insertedCols = NULL;
|
rte->insertedCols = NULL;
|
||||||
rte->updatedCols = NULL;
|
rte->updatedCols = NULL;
|
||||||
|
rte->extraUpdatedCols = NULL;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add completed RTE to pstate's range table list, but not to join list
|
* 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->selectedCols = NULL;
|
||||||
rte->insertedCols = NULL;
|
rte->insertedCols = NULL;
|
||||||
rte->updatedCols = NULL;
|
rte->updatedCols = NULL;
|
||||||
|
rte->extraUpdatedCols = NULL;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add completed RTE to pstate's range table list, but not to join list
|
* 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->selectedCols = NULL;
|
||||||
rte->insertedCols = NULL;
|
rte->insertedCols = NULL;
|
||||||
rte->updatedCols = NULL;
|
rte->updatedCols = NULL;
|
||||||
|
rte->extraUpdatedCols = NULL;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add completed RTE to pstate's range table list, but not to join list
|
* 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->selectedCols = NULL;
|
||||||
rte->insertedCols = NULL;
|
rte->insertedCols = NULL;
|
||||||
rte->updatedCols = NULL;
|
rte->updatedCols = NULL;
|
||||||
|
rte->extraUpdatedCols = NULL;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add completed RTE to pstate's range table list, but not to join list
|
* 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->selectedCols = NULL;
|
||||||
rte->insertedCols = NULL;
|
rte->insertedCols = NULL;
|
||||||
rte->updatedCols = NULL;
|
rte->updatedCols = NULL;
|
||||||
|
rte->extraUpdatedCols = NULL;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add completed RTE to pstate's range table list, but not to join list
|
* 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->selectedCols = NULL;
|
||||||
rte->insertedCols = NULL;
|
rte->insertedCols = NULL;
|
||||||
rte->updatedCols = NULL;
|
rte->updatedCols = NULL;
|
||||||
|
rte->extraUpdatedCols = NULL;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add completed RTE to pstate's range table list, but not to join list
|
* 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->selectedCols = NULL;
|
||||||
rte->insertedCols = NULL;
|
rte->insertedCols = NULL;
|
||||||
rte->updatedCols = NULL;
|
rte->updatedCols = NULL;
|
||||||
|
rte->extraUpdatedCols = NULL;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add completed RTE to pstate's range table list, but not to join list
|
* Add completed RTE to pstate's range table list, but not to join list
|
||||||
|
|
|
@ -502,6 +502,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
|
||||||
bool saw_nullable;
|
bool saw_nullable;
|
||||||
bool saw_default;
|
bool saw_default;
|
||||||
bool saw_identity;
|
bool saw_identity;
|
||||||
|
bool saw_generated;
|
||||||
ListCell *clist;
|
ListCell *clist;
|
||||||
|
|
||||||
cxt->columns = lappend(cxt->columns, column);
|
cxt->columns = lappend(cxt->columns, column);
|
||||||
|
@ -609,6 +610,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
|
||||||
saw_nullable = false;
|
saw_nullable = false;
|
||||||
saw_default = false;
|
saw_default = false;
|
||||||
saw_identity = false;
|
saw_identity = false;
|
||||||
|
saw_generated = false;
|
||||||
|
|
||||||
foreach(clist, column->constraints)
|
foreach(clist, column->constraints)
|
||||||
{
|
{
|
||||||
|
@ -689,6 +691,29 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
|
||||||
break;
|
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:
|
case CONSTR_CHECK:
|
||||||
cxt->ckconstraints = lappend(cxt->ckconstraints, constraint);
|
cxt->ckconstraints = lappend(cxt->ckconstraints, constraint);
|
||||||
break;
|
break;
|
||||||
|
@ -755,6 +780,22 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
|
||||||
column->colname, cxt->relation->relname),
|
column->colname, cxt->relation->relname),
|
||||||
parser_errposition(cxt->pstate,
|
parser_errposition(cxt->pstate,
|
||||||
constraint->location)));
|
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
|
* Copy default, if present and the default has been requested
|
||||||
*/
|
*/
|
||||||
if (attribute->atthasdef &&
|
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;
|
Node *this_default = NULL;
|
||||||
AttrDefault *attrdef;
|
AttrDefault *attrdef;
|
||||||
int i;
|
int i;
|
||||||
|
bool found_whole_row;
|
||||||
|
|
||||||
/* Find default in constraint structure */
|
/* Find default in constraint structure */
|
||||||
Assert(constr != NULL);
|
Assert(constr != NULL);
|
||||||
|
@ -1002,12 +1045,27 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
|
||||||
}
|
}
|
||||||
Assert(this_default != NULL);
|
Assert(this_default != NULL);
|
||||||
|
|
||||||
/*
|
def->cooked_default = map_variable_attnos(this_default,
|
||||||
* If default expr could contain any vars, we'd need to fix 'em,
|
1, 0,
|
||||||
* but it can't; so default is ready to apply to child.
|
attmap, tupleDesc->natts,
|
||||||
*/
|
InvalidOid, &found_whole_row);
|
||||||
|
|
||||||
def->cooked_default = this_default;
|
/*
|
||||||
|
* 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))));
|
||||||
|
|
||||||
|
if (attribute->attgenerated &&
|
||||||
|
(table_like_clause->options & CREATE_TABLE_LIKE_GENERATED))
|
||||||
|
def->generated = attribute->attgenerated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -453,7 +453,7 @@ logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple)
|
||||||
|
|
||||||
for (i = 0; i < desc->natts; i++)
|
for (i = 0; i < desc->natts; i++)
|
||||||
{
|
{
|
||||||
if (TupleDescAttr(desc, i)->attisdropped)
|
if (TupleDescAttr(desc, i)->attisdropped || TupleDescAttr(desc, i)->attgenerated)
|
||||||
continue;
|
continue;
|
||||||
nliveatts++;
|
nliveatts++;
|
||||||
}
|
}
|
||||||
|
@ -473,8 +473,7 @@ logicalrep_write_tuple(StringInfo out, Relation rel, HeapTuple tuple)
|
||||||
Form_pg_attribute att = TupleDescAttr(desc, i);
|
Form_pg_attribute att = TupleDescAttr(desc, i);
|
||||||
char *outputstr;
|
char *outputstr;
|
||||||
|
|
||||||
/* skip dropped columns */
|
if (att->attisdropped || att->attgenerated)
|
||||||
if (att->attisdropped)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (isnull[i])
|
if (isnull[i])
|
||||||
|
@ -573,7 +572,7 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
|
||||||
/* send number of live attributes */
|
/* send number of live attributes */
|
||||||
for (i = 0; i < desc->natts; i++)
|
for (i = 0; i < desc->natts; i++)
|
||||||
{
|
{
|
||||||
if (TupleDescAttr(desc, i)->attisdropped)
|
if (TupleDescAttr(desc, i)->attisdropped || TupleDescAttr(desc, i)->attgenerated)
|
||||||
continue;
|
continue;
|
||||||
nliveatts++;
|
nliveatts++;
|
||||||
}
|
}
|
||||||
|
@ -591,7 +590,7 @@ logicalrep_write_attrs(StringInfo out, Relation rel)
|
||||||
Form_pg_attribute att = TupleDescAttr(desc, i);
|
Form_pg_attribute att = TupleDescAttr(desc, i);
|
||||||
uint8 flags = 0;
|
uint8 flags = 0;
|
||||||
|
|
||||||
if (att->attisdropped)
|
if (att->attisdropped || att->attgenerated)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
/* REPLICA IDENTITY FULL means all columns are sent as part of key. */
|
/* REPLICA IDENTITY FULL means all columns are sent as part of key. */
|
||||||
|
|
|
@ -276,7 +276,7 @@ logicalrep_rel_open(LogicalRepRelId remoteid, LOCKMODE lockmode)
|
||||||
int attnum;
|
int attnum;
|
||||||
Form_pg_attribute attr = TupleDescAttr(desc, i);
|
Form_pg_attribute attr = TupleDescAttr(desc, i);
|
||||||
|
|
||||||
if (attr->attisdropped)
|
if (attr->attisdropped || attr->attgenerated)
|
||||||
{
|
{
|
||||||
entry->attrmap[i] = -1;
|
entry->attrmap[i] = -1;
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -697,10 +697,12 @@ fetch_remote_table_info(char *nspname, char *relname,
|
||||||
" LEFT JOIN pg_catalog.pg_index i"
|
" LEFT JOIN pg_catalog.pg_index i"
|
||||||
" ON (i.indexrelid = pg_get_replica_identity_index(%u))"
|
" ON (i.indexrelid = pg_get_replica_identity_index(%u))"
|
||||||
" WHERE a.attnum > 0::pg_catalog.int2"
|
" WHERE a.attnum > 0::pg_catalog.int2"
|
||||||
" AND NOT a.attisdropped"
|
" AND NOT a.attisdropped %s"
|
||||||
" AND a.attrelid = %u"
|
" AND a.attrelid = %u"
|
||||||
" ORDER BY a.attnum",
|
" 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);
|
res = walrcv_exec(wrconn, cmd.data, 4, attrRow);
|
||||||
|
|
||||||
if (res->status != WALRCV_OK_TUPLES)
|
if (res->status != WALRCV_OK_TUPLES)
|
||||||
|
|
|
@ -236,7 +236,7 @@ slot_fill_defaults(LogicalRepRelMapEntry *rel, EState *estate,
|
||||||
{
|
{
|
||||||
Expr *defexpr;
|
Expr *defexpr;
|
||||||
|
|
||||||
if (TupleDescAttr(desc, attnum)->attisdropped)
|
if (TupleDescAttr(desc, attnum)->attisdropped || TupleDescAttr(desc, attnum)->attgenerated)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (rel->attrmap[attnum] >= 0)
|
if (rel->attrmap[attnum] >= 0)
|
||||||
|
|
|
@ -276,7 +276,7 @@ maybe_send_schema(LogicalDecodingContext *ctx,
|
||||||
{
|
{
|
||||||
Form_pg_attribute att = TupleDescAttr(desc, i);
|
Form_pg_attribute att = TupleDescAttr(desc, i);
|
||||||
|
|
||||||
if (att->attisdropped)
|
if (att->attisdropped || att->attgenerated)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (att->atttypid < FirstNormalObjectId)
|
if (att->atttypid < FirstNormalObjectId)
|
||||||
|
|
|
@ -818,6 +818,13 @@ rewriteTargetListIU(List *targetList,
|
||||||
|
|
||||||
if (att_tup->attidentity == ATTRIBUTE_IDENTITY_BY_DEFAULT && override == OVERRIDING_USER_VALUE)
|
if (att_tup->attidentity == ATTRIBUTE_IDENTITY_BY_DEFAULT && override == OVERRIDING_USER_VALUE)
|
||||||
apply_default = true;
|
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)
|
if (commandType == CMD_UPDATE)
|
||||||
|
@ -828,9 +835,23 @@ rewriteTargetListIU(List *targetList,
|
||||||
errmsg("column \"%s\" can only be updated to DEFAULT", NameStr(att_tup->attname)),
|
errmsg("column \"%s\" can only be updated to DEFAULT", NameStr(att_tup->attname)),
|
||||||
errdetail("Column \"%s\" is an identity column defined as GENERATED ALWAYS.",
|
errdetail("Column \"%s\" is an identity column defined as GENERATED ALWAYS.",
|
||||||
NameStr(att_tup->attname))));
|
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;
|
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. But
|
||||||
/*
|
* not for generated columns.
|
||||||
* No per-column default, so look for a default for the type itself.
|
*/
|
||||||
*/
|
if (expr == NULL && !att_tup->attgenerated)
|
||||||
expr = get_typdefault(atttype);
|
expr = get_typdefault(atttype);
|
||||||
}
|
|
||||||
|
|
||||||
if (expr == NULL)
|
if (expr == NULL)
|
||||||
return NULL; /* No default anywhere */
|
return NULL; /* No default anywhere */
|
||||||
|
@ -1720,12 +1740,14 @@ ApplyRetrieveRule(Query *parsetree,
|
||||||
subrte->selectedCols = rte->selectedCols;
|
subrte->selectedCols = rte->selectedCols;
|
||||||
subrte->insertedCols = rte->insertedCols;
|
subrte->insertedCols = rte->insertedCols;
|
||||||
subrte->updatedCols = rte->updatedCols;
|
subrte->updatedCols = rte->updatedCols;
|
||||||
|
subrte->extraUpdatedCols = rte->extraUpdatedCols;
|
||||||
|
|
||||||
rte->requiredPerms = 0; /* no permission check on subquery itself */
|
rte->requiredPerms = 0; /* no permission check on subquery itself */
|
||||||
rte->checkAsUser = InvalidOid;
|
rte->checkAsUser = InvalidOid;
|
||||||
rte->selectedCols = NULL;
|
rte->selectedCols = NULL;
|
||||||
rte->insertedCols = NULL;
|
rte->insertedCols = NULL;
|
||||||
rte->updatedCols = NULL;
|
rte->updatedCols = NULL;
|
||||||
|
rte->extraUpdatedCols = NULL;
|
||||||
|
|
||||||
return parsetree;
|
return parsetree;
|
||||||
}
|
}
|
||||||
|
|
|
@ -821,6 +821,39 @@ get_attnum(Oid relid, const char *attname)
|
||||||
return InvalidAttrNumber;
|
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
|
* get_atttype
|
||||||
*
|
*
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
#include "nodes/nodeFuncs.h"
|
#include "nodes/nodeFuncs.h"
|
||||||
#include "optimizer/optimizer.h"
|
#include "optimizer/optimizer.h"
|
||||||
#include "partitioning/partbounds.h"
|
#include "partitioning/partbounds.h"
|
||||||
|
#include "rewrite/rewriteHandler.h"
|
||||||
#include "utils/builtins.h"
|
#include "utils/builtins.h"
|
||||||
#include "utils/datum.h"
|
#include "utils/datum.h"
|
||||||
#include "utils/lsyscache.h"
|
#include "utils/lsyscache.h"
|
||||||
|
|
|
@ -515,6 +515,7 @@ RelationBuildTupleDesc(Relation relation)
|
||||||
constr = (TupleConstr *) MemoryContextAlloc(CacheMemoryContext,
|
constr = (TupleConstr *) MemoryContextAlloc(CacheMemoryContext,
|
||||||
sizeof(TupleConstr));
|
sizeof(TupleConstr));
|
||||||
constr->has_not_null = false;
|
constr->has_not_null = false;
|
||||||
|
constr->has_generated_stored = false;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Form a scan key that selects only user attributes (attnum > 0).
|
* Form a scan key that selects only user attributes (attnum > 0).
|
||||||
|
@ -567,6 +568,8 @@ RelationBuildTupleDesc(Relation relation)
|
||||||
/* Update constraint/default info */
|
/* Update constraint/default info */
|
||||||
if (attp->attnotnull)
|
if (attp->attnotnull)
|
||||||
constr->has_not_null = true;
|
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 the column has a default, fill it into the attrdef array */
|
||||||
if (attp->atthasdef)
|
if (attp->atthasdef)
|
||||||
|
@ -3281,6 +3284,7 @@ RelationBuildLocalRelation(const char *relname,
|
||||||
Form_pg_attribute datt = TupleDescAttr(rel->rd_att, i);
|
Form_pg_attribute datt = TupleDescAttr(rel->rd_att, i);
|
||||||
|
|
||||||
datt->attidentity = satt->attidentity;
|
datt->attidentity = satt->attidentity;
|
||||||
|
datt->attgenerated = satt->attgenerated;
|
||||||
datt->attnotnull = satt->attnotnull;
|
datt->attnotnull = satt->attnotnull;
|
||||||
has_not_null |= satt->attnotnull;
|
has_not_null |= satt->attnotnull;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2051,6 +2051,11 @@ dumpTableData_insert(Archive *fout, void *dcontext)
|
||||||
{
|
{
|
||||||
if (field > 0)
|
if (field > 0)
|
||||||
archputs(", ", fout);
|
archputs(", ", fout);
|
||||||
|
if (tbinfo->attgenerated[field])
|
||||||
|
{
|
||||||
|
archputs("DEFAULT", fout);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (PQgetisnull(res, tuple, field))
|
if (PQgetisnull(res, tuple, field))
|
||||||
{
|
{
|
||||||
archputs("NULL", fout);
|
archputs("NULL", fout);
|
||||||
|
@ -8219,6 +8224,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
|
||||||
int i_attnotnull;
|
int i_attnotnull;
|
||||||
int i_atthasdef;
|
int i_atthasdef;
|
||||||
int i_attidentity;
|
int i_attidentity;
|
||||||
|
int i_attgenerated;
|
||||||
int i_attisdropped;
|
int i_attisdropped;
|
||||||
int i_attlen;
|
int i_attlen;
|
||||||
int i_attalign;
|
int i_attalign;
|
||||||
|
@ -8272,6 +8278,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
|
||||||
"a.attislocal,\n"
|
"a.attislocal,\n"
|
||||||
"pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\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)
|
if (fout->remoteVersion >= 110000)
|
||||||
appendPQExpBuffer(q,
|
appendPQExpBuffer(q,
|
||||||
"CASE WHEN a.atthasmissing AND NOT a.attisdropped "
|
"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_attnotnull = PQfnumber(res, "attnotnull");
|
||||||
i_atthasdef = PQfnumber(res, "atthasdef");
|
i_atthasdef = PQfnumber(res, "atthasdef");
|
||||||
i_attidentity = PQfnumber(res, "attidentity");
|
i_attidentity = PQfnumber(res, "attidentity");
|
||||||
|
i_attgenerated = PQfnumber(res, "attgenerated");
|
||||||
i_attisdropped = PQfnumber(res, "attisdropped");
|
i_attisdropped = PQfnumber(res, "attisdropped");
|
||||||
i_attlen = PQfnumber(res, "attlen");
|
i_attlen = PQfnumber(res, "attlen");
|
||||||
i_attalign = PQfnumber(res, "attalign");
|
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->attstorage = (char *) pg_malloc(ntups * sizeof(char));
|
||||||
tbinfo->typstorage = (char *) pg_malloc(ntups * sizeof(char));
|
tbinfo->typstorage = (char *) pg_malloc(ntups * sizeof(char));
|
||||||
tbinfo->attidentity = (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->attisdropped = (bool *) pg_malloc(ntups * sizeof(bool));
|
||||||
tbinfo->attlen = (int *) pg_malloc(ntups * sizeof(int));
|
tbinfo->attlen = (int *) pg_malloc(ntups * sizeof(int));
|
||||||
tbinfo->attalign = (char *) pg_malloc(ntups * sizeof(char));
|
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->attstorage[j] = *(PQgetvalue(res, j, i_attstorage));
|
||||||
tbinfo->typstorage[j] = *(PQgetvalue(res, j, i_typstorage));
|
tbinfo->typstorage[j] = *(PQgetvalue(res, j, i_typstorage));
|
||||||
tbinfo->attidentity[j] = *(PQgetvalue(res, j, i_attidentity));
|
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->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS);
|
||||||
tbinfo->attisdropped[j] = (PQgetvalue(res, j, i_attisdropped)[0] == 't');
|
tbinfo->attisdropped[j] = (PQgetvalue(res, j, i_attisdropped)[0] == 't');
|
||||||
tbinfo->attlen[j] = atoi(PQgetvalue(res, j, i_attlen));
|
tbinfo->attlen[j] = atoi(PQgetvalue(res, j, i_attlen));
|
||||||
|
@ -15708,6 +15724,20 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
|
||||||
tbinfo->atttypnames[j]);
|
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 */
|
/* Add collation if not default for the type */
|
||||||
if (OidIsValid(tbinfo->attcollation[j]))
|
if (OidIsValid(tbinfo->attcollation[j]))
|
||||||
{
|
{
|
||||||
|
@ -15718,13 +15748,6 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
|
||||||
appendPQExpBuffer(q, " COLLATE %s",
|
appendPQExpBuffer(q, " COLLATE %s",
|
||||||
fmtQualifiedDumpable(coll));
|
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;
|
int numatts = ti->numatts;
|
||||||
char **attnames = ti->attnames;
|
char **attnames = ti->attnames;
|
||||||
bool *attisdropped = ti->attisdropped;
|
bool *attisdropped = ti->attisdropped;
|
||||||
|
char *attgenerated = ti->attgenerated;
|
||||||
bool needComma;
|
bool needComma;
|
||||||
int i;
|
int i;
|
||||||
|
|
||||||
|
@ -18312,6 +18336,8 @@ fmtCopyColumnList(const TableInfo *ti, PQExpBuffer buffer)
|
||||||
{
|
{
|
||||||
if (attisdropped[i])
|
if (attisdropped[i])
|
||||||
continue;
|
continue;
|
||||||
|
if (attgenerated[i])
|
||||||
|
continue;
|
||||||
if (needComma)
|
if (needComma)
|
||||||
appendPQExpBufferStr(buffer, ", ");
|
appendPQExpBufferStr(buffer, ", ");
|
||||||
appendPQExpBufferStr(buffer, fmtId(attnames[i]));
|
appendPQExpBufferStr(buffer, fmtId(attnames[i]));
|
||||||
|
|
|
@ -310,6 +310,7 @@ typedef struct _tableInfo
|
||||||
char *typstorage; /* type storage scheme */
|
char *typstorage; /* type storage scheme */
|
||||||
bool *attisdropped; /* true if attr is dropped; don't dump it */
|
bool *attisdropped; /* true if attr is dropped; don't dump it */
|
||||||
char *attidentity;
|
char *attidentity;
|
||||||
|
char *attgenerated;
|
||||||
int *attlen; /* attribute length, used by binary_upgrade */
|
int *attlen; /* attribute length, used by binary_upgrade */
|
||||||
char *attalign; /* attribute align, used by binary_upgrade */
|
char *attalign; /* attribute align, used by binary_upgrade */
|
||||||
bool *attislocal; /* true if attr has local definition */
|
bool *attislocal; /* true if attr has local definition */
|
||||||
|
|
|
@ -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
|
* 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
|
* circular set of foreign key constraints (or a single self-referential
|
||||||
|
|
|
@ -2392,6 +2392,23 @@ my %tests = (
|
||||||
unlike => { exclude_dump_test_schema => 1, },
|
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 TABLE table_with_stats' => {
|
||||||
create_order => 98,
|
create_order => 98,
|
||||||
create_sql => 'CREATE TABLE dump_test.table_index_stats (
|
create_sql => 'CREATE TABLE dump_test.table_index_stats (
|
||||||
|
|
|
@ -1464,6 +1464,7 @@ describeOneTableDetails(const char *schemaname,
|
||||||
attnotnull_col = -1,
|
attnotnull_col = -1,
|
||||||
attcoll_col = -1,
|
attcoll_col = -1,
|
||||||
attidentity_col = -1,
|
attidentity_col = -1,
|
||||||
|
attgenerated_col = -1,
|
||||||
isindexkey_col = -1,
|
isindexkey_col = -1,
|
||||||
indexdef_col = -1,
|
indexdef_col = -1,
|
||||||
fdwopts_col = -1,
|
fdwopts_col = -1,
|
||||||
|
@ -1834,8 +1835,9 @@ describeOneTableDetails(const char *schemaname,
|
||||||
|
|
||||||
if (show_column_details)
|
if (show_column_details)
|
||||||
{
|
{
|
||||||
|
/* use "pretty" mode for expression to avoid excessive parentheses */
|
||||||
appendPQExpBufferStr(&buf,
|
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 FROM pg_catalog.pg_attrdef d"
|
||||||
"\n WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef)"
|
"\n WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef)"
|
||||||
",\n a.attnotnull");
|
",\n a.attnotnull");
|
||||||
|
@ -1852,6 +1854,11 @@ describeOneTableDetails(const char *schemaname,
|
||||||
else
|
else
|
||||||
appendPQExpBufferStr(&buf, ",\n ''::pg_catalog.char AS attidentity");
|
appendPQExpBufferStr(&buf, ",\n ''::pg_catalog.char AS attidentity");
|
||||||
attidentity_col = cols++;
|
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 ||
|
if (tableinfo.relkind == RELKIND_INDEX ||
|
||||||
tableinfo.relkind == RELKIND_PARTITIONED_INDEX)
|
tableinfo.relkind == RELKIND_PARTITIONED_INDEX)
|
||||||
|
@ -2032,6 +2039,7 @@ describeOneTableDetails(const char *schemaname,
|
||||||
if (show_column_details)
|
if (show_column_details)
|
||||||
{
|
{
|
||||||
char *identity;
|
char *identity;
|
||||||
|
char *generated;
|
||||||
char *default_str = "";
|
char *default_str = "";
|
||||||
|
|
||||||
printTableAddCell(&cont, PQgetvalue(res, i, attcoll_col), false, false);
|
printTableAddCell(&cont, PQgetvalue(res, i, attcoll_col), false, false);
|
||||||
|
@ -2041,16 +2049,19 @@ describeOneTableDetails(const char *schemaname,
|
||||||
false, false);
|
false, false);
|
||||||
|
|
||||||
identity = PQgetvalue(res, i, attidentity_col);
|
identity = PQgetvalue(res, i, attidentity_col);
|
||||||
|
generated = PQgetvalue(res, i, attgenerated_col);
|
||||||
|
|
||||||
if (!identity[0])
|
if (identity[0] == ATTRIBUTE_IDENTITY_ALWAYS)
|
||||||
/* (note: above we cut off the 'default' string at 128) */
|
|
||||||
default_str = PQgetvalue(res, i, attrdef_col);
|
|
||||||
else if (identity[0] == ATTRIBUTE_IDENTITY_ALWAYS)
|
|
||||||
default_str = "generated always as identity";
|
default_str = "generated always as identity";
|
||||||
else if (identity[0] == ATTRIBUTE_IDENTITY_BY_DEFAULT)
|
else if (identity[0] == ATTRIBUTE_IDENTITY_BY_DEFAULT)
|
||||||
default_str = "generated by default as identity";
|
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 */
|
/* Info for index columns */
|
||||||
|
|
|
@ -42,6 +42,7 @@ typedef struct TupleConstr
|
||||||
uint16 num_defval;
|
uint16 num_defval;
|
||||||
uint16 num_check;
|
uint16 num_check;
|
||||||
bool has_not_null;
|
bool has_not_null;
|
||||||
|
bool has_generated_stored;
|
||||||
} TupleConstr;
|
} TupleConstr;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -53,6 +53,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* yyyymmddN */
|
/* yyyymmddN */
|
||||||
#define CATALOG_VERSION_NO 201903291
|
#define CATALOG_VERSION_NO 201903301
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -28,6 +28,7 @@ typedef struct RawColumnDefault
|
||||||
AttrNumber attnum; /* attribute to attach default to */
|
AttrNumber attnum; /* attribute to attach default to */
|
||||||
Node *raw_default; /* default value (untransformed parse tree) */
|
Node *raw_default; /* default value (untransformed parse tree) */
|
||||||
bool missingMode; /* true if part of add column processing */
|
bool missingMode; /* true if part of add column processing */
|
||||||
|
char generated; /* attgenerated setting */
|
||||||
} RawColumnDefault;
|
} RawColumnDefault;
|
||||||
|
|
||||||
typedef struct CookedConstraint
|
typedef struct CookedConstraint
|
||||||
|
@ -120,7 +121,8 @@ extern Node *cookDefault(ParseState *pstate,
|
||||||
Node *raw_default,
|
Node *raw_default,
|
||||||
Oid atttypid,
|
Oid atttypid,
|
||||||
int32 atttypmod,
|
int32 atttypmod,
|
||||||
const char *attname);
|
const char *attname,
|
||||||
|
char attgenerated);
|
||||||
|
|
||||||
extern void DeleteRelationTuple(Oid relid);
|
extern void DeleteRelationTuple(Oid relid);
|
||||||
extern void DeleteAttributeTuples(Oid relid);
|
extern void DeleteAttributeTuples(Oid relid);
|
||||||
|
|
|
@ -140,6 +140,9 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
|
||||||
/* One of the ATTRIBUTE_IDENTITY_* constants below, or '\0' */
|
/* One of the ATTRIBUTE_IDENTITY_* constants below, or '\0' */
|
||||||
char attidentity BKI_DEFAULT('\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 */
|
/* Is dropped (ie, logically invisible) or not */
|
||||||
bool attisdropped BKI_DEFAULT(f);
|
bool attisdropped BKI_DEFAULT(f);
|
||||||
|
|
||||||
|
@ -201,6 +204,8 @@ typedef FormData_pg_attribute *Form_pg_attribute;
|
||||||
#define ATTRIBUTE_IDENTITY_ALWAYS 'a'
|
#define ATTRIBUTE_IDENTITY_ALWAYS 'a'
|
||||||
#define ATTRIBUTE_IDENTITY_BY_DEFAULT 'd'
|
#define ATTRIBUTE_IDENTITY_BY_DEFAULT 'd'
|
||||||
|
|
||||||
|
#define ATTRIBUTE_GENERATED_STORED 's'
|
||||||
|
|
||||||
#endif /* EXPOSE_TO_CLIENT_CODE */
|
#endif /* EXPOSE_TO_CLIENT_CODE */
|
||||||
|
|
||||||
#endif /* PG_ATTRIBUTE_H */
|
#endif /* PG_ATTRIBUTE_H */
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
relname => 'pg_attribute', reltype => 'pg_attribute', relam => 'heap',
|
relname => 'pg_attribute', reltype => 'pg_attribute', relam => 'heap',
|
||||||
relfilenode => '0', relpages => '0', reltuples => '0', relallvisible => '0',
|
relfilenode => '0', relpages => '0', reltuples => '0', relallvisible => '0',
|
||||||
reltoastrelid => '0', relhasindex => 'f', relisshared => 'f',
|
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',
|
relhasrules => 'f', relhastriggers => 'f', relhassubclass => 'f',
|
||||||
relrowsecurity => 'f', relforcerowsecurity => 'f', relispopulated => 't',
|
relrowsecurity => 'f', relforcerowsecurity => 'f', relispopulated => 't',
|
||||||
relreplident => 'n', relispartition => 'f', relfrozenxid => '3',
|
relreplident => 'n', relispartition => 'f', relfrozenxid => '3',
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
|
|
||||||
#include "nodes/execnodes.h"
|
#include "nodes/execnodes.h"
|
||||||
|
|
||||||
|
extern void ExecComputeStoredGenerated(EState *estate, TupleTableSlot *slot);
|
||||||
|
|
||||||
extern ModifyTableState *ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags);
|
extern ModifyTableState *ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags);
|
||||||
extern void ExecEndModifyTable(ModifyTableState *node);
|
extern void ExecEndModifyTable(ModifyTableState *node);
|
||||||
extern void ExecReScanModifyTable(ModifyTableState *node);
|
extern void ExecReScanModifyTable(ModifyTableState *node);
|
||||||
|
|
|
@ -452,6 +452,9 @@ typedef struct ResultRelInfo
|
||||||
/* array of constraint-checking expr states */
|
/* array of constraint-checking expr states */
|
||||||
ExprState **ri_ConstraintExprs;
|
ExprState **ri_ConstraintExprs;
|
||||||
|
|
||||||
|
/* array of stored generated columns expr states */
|
||||||
|
ExprState **ri_GeneratedExprs;
|
||||||
|
|
||||||
/* for removing junk attributes from tuples */
|
/* for removing junk attributes from tuples */
|
||||||
JunkFilter *ri_junkFilter;
|
JunkFilter *ri_junkFilter;
|
||||||
|
|
||||||
|
|
|
@ -655,6 +655,7 @@ typedef struct ColumnDef
|
||||||
char identity; /* attidentity setting */
|
char identity; /* attidentity setting */
|
||||||
RangeVar *identitySequence; /* to store identity sequence name for
|
RangeVar *identitySequence; /* to store identity sequence name for
|
||||||
* ALTER TABLE ... ADD COLUMN */
|
* ALTER TABLE ... ADD COLUMN */
|
||||||
|
char generated; /* attgenerated setting */
|
||||||
CollateClause *collClause; /* untransformed COLLATE spec, if any */
|
CollateClause *collClause; /* untransformed COLLATE spec, if any */
|
||||||
Oid collOid; /* collation OID (InvalidOid if not set) */
|
Oid collOid; /* collation OID (InvalidOid if not set) */
|
||||||
List *constraints; /* other constraints on column */
|
List *constraints; /* other constraints on column */
|
||||||
|
@ -677,10 +678,11 @@ typedef enum TableLikeOption
|
||||||
CREATE_TABLE_LIKE_COMMENTS = 1 << 0,
|
CREATE_TABLE_LIKE_COMMENTS = 1 << 0,
|
||||||
CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 1,
|
CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 1,
|
||||||
CREATE_TABLE_LIKE_DEFAULTS = 1 << 2,
|
CREATE_TABLE_LIKE_DEFAULTS = 1 << 2,
|
||||||
CREATE_TABLE_LIKE_IDENTITY = 1 << 3,
|
CREATE_TABLE_LIKE_GENERATED = 1 << 3,
|
||||||
CREATE_TABLE_LIKE_INDEXES = 1 << 4,
|
CREATE_TABLE_LIKE_IDENTITY = 1 << 4,
|
||||||
CREATE_TABLE_LIKE_STATISTICS = 1 << 5,
|
CREATE_TABLE_LIKE_INDEXES = 1 << 5,
|
||||||
CREATE_TABLE_LIKE_STORAGE = 1 << 6,
|
CREATE_TABLE_LIKE_STATISTICS = 1 << 6,
|
||||||
|
CREATE_TABLE_LIKE_STORAGE = 1 << 7,
|
||||||
CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
|
CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
|
||||||
} TableLikeOption;
|
} TableLikeOption;
|
||||||
|
|
||||||
|
@ -933,6 +935,15 @@ typedef struct PartitionCmd
|
||||||
* them in these fields. A whole-row Var reference is represented by
|
* them in these fields. A whole-row Var reference is represented by
|
||||||
* setting the bit for InvalidAttrNumber.
|
* 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),
|
* securityQuals is a list of security barrier quals (boolean expressions),
|
||||||
* to be tested in the listed order before returning a row from the
|
* 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
|
* 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 *selectedCols; /* columns needing SELECT permission */
|
||||||
Bitmapset *insertedCols; /* columns needing INSERT permission */
|
Bitmapset *insertedCols; /* columns needing INSERT permission */
|
||||||
Bitmapset *updatedCols; /* columns needing UPDATE permission */
|
Bitmapset *updatedCols; /* columns needing UPDATE permission */
|
||||||
|
Bitmapset *extraUpdatedCols; /* generated columns being updated */
|
||||||
List *securityQuals; /* security barrier quals to apply, if any */
|
List *securityQuals; /* security barrier quals to apply, if any */
|
||||||
} RangeTblEntry;
|
} RangeTblEntry;
|
||||||
|
|
||||||
|
@ -2086,6 +2098,7 @@ typedef enum ConstrType /* types of constraints */
|
||||||
CONSTR_NOTNULL,
|
CONSTR_NOTNULL,
|
||||||
CONSTR_DEFAULT,
|
CONSTR_DEFAULT,
|
||||||
CONSTR_IDENTITY,
|
CONSTR_IDENTITY,
|
||||||
|
CONSTR_GENERATED,
|
||||||
CONSTR_CHECK,
|
CONSTR_CHECK,
|
||||||
CONSTR_PRIMARY,
|
CONSTR_PRIMARY,
|
||||||
CONSTR_UNIQUE,
|
CONSTR_UNIQUE,
|
||||||
|
@ -2124,7 +2137,8 @@ typedef struct Constraint
|
||||||
bool is_no_inherit; /* is constraint non-inheritable? */
|
bool is_no_inherit; /* is constraint non-inheritable? */
|
||||||
Node *raw_expr; /* expr, as untransformed parse tree */
|
Node *raw_expr; /* expr, as untransformed parse tree */
|
||||||
char *cooked_expr; /* expr, as nodeToString representation */
|
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): */
|
/* Fields used for unique constraints (UNIQUE and PRIMARY KEY): */
|
||||||
List *keys; /* String nodes naming referenced key
|
List *keys; /* String nodes naming referenced key
|
||||||
|
|
|
@ -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_row_triggers(PlannerInfo *root, Index rti, CmdType event);
|
||||||
|
|
||||||
|
extern bool has_stored_generated_columns(PlannerInfo *root, Index rti);
|
||||||
|
|
||||||
#endif /* PLANCAT_H */
|
#endif /* PLANCAT_H */
|
||||||
|
|
|
@ -383,6 +383,7 @@ PG_KEYWORD("statistics", STATISTICS, UNRESERVED_KEYWORD)
|
||||||
PG_KEYWORD("stdin", STDIN, UNRESERVED_KEYWORD)
|
PG_KEYWORD("stdin", STDIN, UNRESERVED_KEYWORD)
|
||||||
PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD)
|
PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD)
|
||||||
PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD)
|
PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD)
|
||||||
|
PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD)
|
||||||
PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD)
|
PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD)
|
||||||
PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD)
|
PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD)
|
||||||
PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD)
|
PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD)
|
||||||
|
|
|
@ -71,7 +71,8 @@ typedef enum ParseExprKind
|
||||||
EXPR_KIND_PARTITION_BOUND, /* partition bound expression */
|
EXPR_KIND_PARTITION_BOUND, /* partition bound expression */
|
||||||
EXPR_KIND_PARTITION_EXPRESSION, /* PARTITION BY expression */
|
EXPR_KIND_PARTITION_EXPRESSION, /* PARTITION BY expression */
|
||||||
EXPR_KIND_CALL_ARGUMENT, /* procedure argument in CALL */
|
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;
|
} ParseExprKind;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,7 @@ extern Oid get_opfamily_proc(Oid opfamily, Oid lefttype, Oid righttype,
|
||||||
int16 procnum);
|
int16 procnum);
|
||||||
extern char *get_attname(Oid relid, AttrNumber attnum, bool missing_ok);
|
extern char *get_attname(Oid relid, AttrNumber attnum, bool missing_ok);
|
||||||
extern AttrNumber get_attnum(Oid relid, const char *attname);
|
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 Oid get_atttype(Oid relid, AttrNumber attnum);
|
||||||
extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum,
|
extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum,
|
||||||
Oid *typid, int32 *typmod, Oid *collid);
|
Oid *typid, int32 *typmod, Oid *collid);
|
||||||
|
|
|
@ -6,6 +6,10 @@ CREATE TABLE trigger_test (
|
||||||
v varchar,
|
v varchar,
|
||||||
foo rowcompnest
|
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 $$
|
CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger LANGUAGE plperl AS $$
|
||||||
|
|
||||||
# make sure keys are sorted for consistent results - perl no longer
|
# 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->{table_schema} = 'public'
|
||||||
NOTICE: $_TD->{when} = 'BEFORE'
|
NOTICE: $_TD->{when} = 'BEFORE'
|
||||||
DROP TRIGGER show_trigger_data_trig on 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);
|
||||||
|
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)")');
|
insert into trigger_test values(1,'insert', '("(1)")');
|
||||||
CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
|
CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
|
||||||
CREATE TRIGGER show_trigger_data_trig
|
CREATE TRIGGER show_trigger_data_trig
|
||||||
|
@ -295,3 +372,21 @@ NOTICE: perlsnitch: ddl_command_start DROP TABLE
|
||||||
NOTICE: perlsnitch: ddl_command_end DROP TABLE
|
NOTICE: perlsnitch: ddl_command_end DROP TABLE
|
||||||
drop event trigger perl_a_snitch;
|
drop event trigger perl_a_snitch;
|
||||||
drop event trigger perl_b_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)
|
||||||
|
|
||||||
|
|
|
@ -266,7 +266,7 @@ static plperl_proc_desc *compile_plperl_function(Oid fn_oid,
|
||||||
bool is_trigger,
|
bool is_trigger,
|
||||||
bool is_event_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_hash_from_datum(Datum attr);
|
||||||
static SV *plperl_ref_from_pg_array(Datum arg, Oid typid);
|
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);
|
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, "name", cstr2sv(tdata->tg_trigger->tgname));
|
||||||
hv_store_string(hv, "relid", cstr2sv(relid));
|
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))
|
if (TRIGGER_FIRED_BY_INSERT(tdata->tg_event))
|
||||||
{
|
{
|
||||||
event = "INSERT";
|
event = "INSERT";
|
||||||
if (TRIGGER_FIRED_FOR_ROW(tdata->tg_event))
|
if (TRIGGER_FIRED_FOR_ROW(tdata->tg_event))
|
||||||
hv_store_string(hv, "new",
|
hv_store_string(hv, "new",
|
||||||
plperl_hash_from_tuple(tdata->tg_trigtuple,
|
plperl_hash_from_tuple(tdata->tg_trigtuple,
|
||||||
tupdesc));
|
tupdesc,
|
||||||
|
!TRIGGER_FIRED_BEFORE(tdata->tg_event)));
|
||||||
}
|
}
|
||||||
else if (TRIGGER_FIRED_BY_DELETE(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))
|
if (TRIGGER_FIRED_FOR_ROW(tdata->tg_event))
|
||||||
hv_store_string(hv, "old",
|
hv_store_string(hv, "old",
|
||||||
plperl_hash_from_tuple(tdata->tg_trigtuple,
|
plperl_hash_from_tuple(tdata->tg_trigtuple,
|
||||||
tupdesc));
|
tupdesc,
|
||||||
|
true));
|
||||||
}
|
}
|
||||||
else if (TRIGGER_FIRED_BY_UPDATE(tdata->tg_event))
|
else if (TRIGGER_FIRED_BY_UPDATE(tdata->tg_event))
|
||||||
{
|
{
|
||||||
|
@ -1667,10 +1674,12 @@ plperl_trigger_build_args(FunctionCallInfo fcinfo)
|
||||||
{
|
{
|
||||||
hv_store_string(hv, "old",
|
hv_store_string(hv, "old",
|
||||||
plperl_hash_from_tuple(tdata->tg_trigtuple,
|
plperl_hash_from_tuple(tdata->tg_trigtuple,
|
||||||
tupdesc));
|
tupdesc,
|
||||||
|
true));
|
||||||
hv_store_string(hv, "new",
|
hv_store_string(hv, "new",
|
||||||
plperl_hash_from_tuple(tdata->tg_newtuple,
|
plperl_hash_from_tuple(tdata->tg_newtuple,
|
||||||
tupdesc));
|
tupdesc,
|
||||||
|
!TRIGGER_FIRED_BEFORE(tdata->tg_event)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (TRIGGER_FIRED_BY_TRUNCATE(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),
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||||
errmsg("cannot set system attribute \"%s\"",
|
errmsg("cannot set system attribute \"%s\"",
|
||||||
key)));
|
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,
|
modvalues[attn - 1] = plperl_sv_to_datum(val,
|
||||||
attr->atttypid,
|
attr->atttypid,
|
||||||
|
@ -3012,7 +3026,7 @@ plperl_hash_from_datum(Datum attr)
|
||||||
tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
|
tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
|
||||||
tmptup.t_data = td;
|
tmptup.t_data = td;
|
||||||
|
|
||||||
sv = plperl_hash_from_tuple(&tmptup, tupdesc);
|
sv = plperl_hash_from_tuple(&tmptup, tupdesc, true);
|
||||||
ReleaseTupleDesc(tupdesc);
|
ReleaseTupleDesc(tupdesc);
|
||||||
|
|
||||||
return sv;
|
return sv;
|
||||||
|
@ -3020,7 +3034,7 @@ plperl_hash_from_datum(Datum attr)
|
||||||
|
|
||||||
/* Build a hash from all attributes of a given tuple. */
|
/* Build a hash from all attributes of a given tuple. */
|
||||||
static SV *
|
static SV *
|
||||||
plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc)
|
plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc, bool include_generated)
|
||||||
{
|
{
|
||||||
dTHX;
|
dTHX;
|
||||||
HV *hv;
|
HV *hv;
|
||||||
|
@ -3044,6 +3058,13 @@ plperl_hash_from_tuple(HeapTuple tuple, TupleDesc tupdesc)
|
||||||
if (att->attisdropped)
|
if (att->attisdropped)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (att->attgenerated)
|
||||||
|
{
|
||||||
|
/* don't include unless requested */
|
||||||
|
if (!include_generated)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
attname = NameStr(att->attname);
|
attname = NameStr(att->attname);
|
||||||
attr = heap_getattr(tuple, i + 1, tupdesc, &isnull);
|
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);
|
av_extend(rows, processed);
|
||||||
for (i = 0; i < processed; i++)
|
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);
|
av_push(rows, row);
|
||||||
}
|
}
|
||||||
hv_store_string(result, "rows",
|
hv_store_string(result, "rows",
|
||||||
|
@ -3484,7 +3505,8 @@ plperl_spi_fetchrow(char *cursor)
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
row = plperl_hash_from_tuple(SPI_tuptable->vals[0],
|
row = plperl_hash_from_tuple(SPI_tuptable->vals[0],
|
||||||
SPI_tuptable->tupdesc);
|
SPI_tuptable->tupdesc,
|
||||||
|
true);
|
||||||
}
|
}
|
||||||
SPI_freetuptable(SPI_tuptable);
|
SPI_freetuptable(SPI_tuptable);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,11 @@ CREATE TABLE trigger_test (
|
||||||
foo rowcompnest
|
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 $$
|
CREATE OR REPLACE FUNCTION trigger_data() RETURNS trigger LANGUAGE plperl AS $$
|
||||||
|
|
||||||
# make sure keys are sorted for consistent results - perl no longer
|
# 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;
|
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)")');
|
insert into trigger_test values(1,'insert', '("(1)")');
|
||||||
CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
|
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_a_snitch;
|
||||||
drop event trigger perl_b_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;
|
||||||
|
|
|
@ -924,6 +924,26 @@ plpgsql_exec_trigger(PLpgSQL_function *func,
|
||||||
false, false);
|
false, false);
|
||||||
expanded_record_set_tuple(rec_old->erh, trigdata->tg_trigtuple,
|
expanded_record_set_tuple(rec_old->erh, trigdata->tg_trigtuple,
|
||||||
false, false);
|
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))
|
else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
|
||||||
{
|
{
|
||||||
|
|
|
@ -67,6 +67,10 @@ SELECT * FROM users;
|
||||||
-- dump trigger data
|
-- dump trigger data
|
||||||
CREATE TABLE trigger_test
|
CREATE TABLE trigger_test
|
||||||
(i int, v text );
|
(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 $$
|
CREATE FUNCTION trigger_data() RETURNS trigger LANGUAGE plpythonu AS $$
|
||||||
|
|
||||||
if 'relid' in TD:
|
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_stmt on trigger_test;
|
||||||
DROP TRIGGER show_trigger_data_trig_before on trigger_test;
|
DROP TRIGGER show_trigger_data_trig_before on trigger_test;
|
||||||
DROP TRIGGER show_trigger_data_trig_after 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');
|
insert into trigger_test values(1,'insert');
|
||||||
CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
|
CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
|
||||||
CREATE TRIGGER show_trigger_data_trig
|
CREATE TRIGGER show_trigger_data_trig
|
||||||
|
@ -524,3 +599,22 @@ INFO: old: 1 -> a
|
||||||
INFO: new: 1 -> b
|
INFO: new: 1 -> b
|
||||||
DROP TABLE transition_table_test;
|
DROP TABLE transition_table_test;
|
||||||
DROP FUNCTION transition_table_test_f();
|
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)
|
||||||
|
|
||||||
|
|
|
@ -357,7 +357,7 @@ PLy_cursor_iternext(PyObject *self)
|
||||||
exec_ctx->curr_proc);
|
exec_ctx->curr_proc);
|
||||||
|
|
||||||
ret = PLy_input_from_tuple(&cursor->result, SPI_tuptable->vals[0],
|
ret = PLy_input_from_tuple(&cursor->result, SPI_tuptable->vals[0],
|
||||||
SPI_tuptable->tupdesc);
|
SPI_tuptable->tupdesc, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
SPI_freetuptable(SPI_tuptable);
|
SPI_freetuptable(SPI_tuptable);
|
||||||
|
@ -453,7 +453,8 @@ PLy_cursor_fetch(PyObject *self, PyObject *args)
|
||||||
{
|
{
|
||||||
PyObject *row = PLy_input_from_tuple(&cursor->result,
|
PyObject *row = PLy_input_from_tuple(&cursor->result,
|
||||||
SPI_tuptable->vals[i],
|
SPI_tuptable->vals[i],
|
||||||
SPI_tuptable->tupdesc);
|
SPI_tuptable->tupdesc,
|
||||||
|
true);
|
||||||
|
|
||||||
PyList_SetItem(ret->rows, i, row);
|
PyList_SetItem(ret->rows, i, row);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
#include "executor/spi.h"
|
#include "executor/spi.h"
|
||||||
#include "funcapi.h"
|
#include "funcapi.h"
|
||||||
#include "utils/builtins.h"
|
#include "utils/builtins.h"
|
||||||
|
#include "utils/lsyscache.h"
|
||||||
#include "utils/rel.h"
|
#include "utils/rel.h"
|
||||||
#include "utils/typcache.h"
|
#include "utils/typcache.h"
|
||||||
|
|
||||||
|
@ -751,6 +752,11 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
|
||||||
PyDict_SetItemString(pltdata, "level", pltlevel);
|
PyDict_SetItemString(pltdata, "level", pltlevel);
|
||||||
Py_DECREF(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))
|
if (TRIGGER_FIRED_BY_INSERT(tdata->tg_event))
|
||||||
{
|
{
|
||||||
pltevent = PyString_FromString("INSERT");
|
pltevent = PyString_FromString("INSERT");
|
||||||
|
@ -758,7 +764,8 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
|
||||||
PyDict_SetItemString(pltdata, "old", Py_None);
|
PyDict_SetItemString(pltdata, "old", Py_None);
|
||||||
pytnew = PLy_input_from_tuple(&proc->result_in,
|
pytnew = PLy_input_from_tuple(&proc->result_in,
|
||||||
tdata->tg_trigtuple,
|
tdata->tg_trigtuple,
|
||||||
rel_descr);
|
rel_descr,
|
||||||
|
!TRIGGER_FIRED_BEFORE(tdata->tg_event));
|
||||||
PyDict_SetItemString(pltdata, "new", pytnew);
|
PyDict_SetItemString(pltdata, "new", pytnew);
|
||||||
Py_DECREF(pytnew);
|
Py_DECREF(pytnew);
|
||||||
*rv = tdata->tg_trigtuple;
|
*rv = tdata->tg_trigtuple;
|
||||||
|
@ -770,7 +777,8 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
|
||||||
PyDict_SetItemString(pltdata, "new", Py_None);
|
PyDict_SetItemString(pltdata, "new", Py_None);
|
||||||
pytold = PLy_input_from_tuple(&proc->result_in,
|
pytold = PLy_input_from_tuple(&proc->result_in,
|
||||||
tdata->tg_trigtuple,
|
tdata->tg_trigtuple,
|
||||||
rel_descr);
|
rel_descr,
|
||||||
|
true);
|
||||||
PyDict_SetItemString(pltdata, "old", pytold);
|
PyDict_SetItemString(pltdata, "old", pytold);
|
||||||
Py_DECREF(pytold);
|
Py_DECREF(pytold);
|
||||||
*rv = tdata->tg_trigtuple;
|
*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,
|
pytnew = PLy_input_from_tuple(&proc->result_in,
|
||||||
tdata->tg_newtuple,
|
tdata->tg_newtuple,
|
||||||
rel_descr);
|
rel_descr,
|
||||||
|
!TRIGGER_FIRED_BEFORE(tdata->tg_event));
|
||||||
PyDict_SetItemString(pltdata, "new", pytnew);
|
PyDict_SetItemString(pltdata, "new", pytnew);
|
||||||
Py_DECREF(pytnew);
|
Py_DECREF(pytnew);
|
||||||
pytold = PLy_input_from_tuple(&proc->result_in,
|
pytold = PLy_input_from_tuple(&proc->result_in,
|
||||||
tdata->tg_trigtuple,
|
tdata->tg_trigtuple,
|
||||||
rel_descr);
|
rel_descr,
|
||||||
|
true);
|
||||||
PyDict_SetItemString(pltdata, "old", pytold);
|
PyDict_SetItemString(pltdata, "old", pytold);
|
||||||
Py_DECREF(pytold);
|
Py_DECREF(pytold);
|
||||||
*rv = tdata->tg_newtuple;
|
*rv = tdata->tg_newtuple;
|
||||||
|
@ -952,6 +962,11 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata,
|
||||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||||
errmsg("cannot set system attribute \"%s\"",
|
errmsg("cannot set system attribute \"%s\"",
|
||||||
plattstr)));
|
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);
|
plval = PyDict_GetItem(plntup, platt);
|
||||||
if (plval == NULL)
|
if (plval == NULL)
|
||||||
|
|
|
@ -419,7 +419,8 @@ PLy_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 rows, int status)
|
||||||
{
|
{
|
||||||
PyObject *row = PLy_input_from_tuple(&ininfo,
|
PyObject *row = PLy_input_from_tuple(&ininfo,
|
||||||
tuptable->vals[i],
|
tuptable->vals[i],
|
||||||
tuptable->tupdesc);
|
tuptable->tupdesc,
|
||||||
|
true);
|
||||||
|
|
||||||
PyList_SetItem(result->rows, i, row);
|
PyList_SetItem(result->rows, i, row);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
static PyObject *PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
|
||||||
char **dataptr_p, bits8 **bitmap_p, int *bitmask_p);
|
char **dataptr_p, bits8 **bitmap_p, int *bitmask_p);
|
||||||
static PyObject *PLyDict_FromComposite(PLyDatumToOb *arg, Datum d);
|
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 */
|
/* conversion from Python objects to Datums */
|
||||||
static Datum PLyObject_ToBool(PLyObToDatum *arg, PyObject *plrv,
|
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.
|
* but in practice all callers have the right tupdesc available.
|
||||||
*/
|
*/
|
||||||
PyObject *
|
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;
|
PyObject *dict;
|
||||||
PLyExecutionContext *exec_ctx = PLy_current_execution_context();
|
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);
|
oldcontext = MemoryContextSwitchTo(scratch_context);
|
||||||
|
|
||||||
dict = PLyDict_FromTuple(arg, tuple, desc);
|
dict = PLyDict_FromTuple(arg, tuple, desc, include_generated);
|
||||||
|
|
||||||
MemoryContextSwitchTo(oldcontext);
|
MemoryContextSwitchTo(oldcontext);
|
||||||
|
|
||||||
|
@ -804,7 +804,7 @@ PLyDict_FromComposite(PLyDatumToOb *arg, Datum d)
|
||||||
tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
|
tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
|
||||||
tmptup.t_data = td;
|
tmptup.t_data = td;
|
||||||
|
|
||||||
dict = PLyDict_FromTuple(arg, &tmptup, tupdesc);
|
dict = PLyDict_FromTuple(arg, &tmptup, tupdesc, true);
|
||||||
|
|
||||||
ReleaseTupleDesc(tupdesc);
|
ReleaseTupleDesc(tupdesc);
|
||||||
|
|
||||||
|
@ -815,7 +815,7 @@ PLyDict_FromComposite(PLyDatumToOb *arg, Datum d)
|
||||||
* Transform a tuple into a Python dict object.
|
* Transform a tuple into a Python dict object.
|
||||||
*/
|
*/
|
||||||
static PyObject *
|
static PyObject *
|
||||||
PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc)
|
PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool include_generated)
|
||||||
{
|
{
|
||||||
PyObject *volatile dict;
|
PyObject *volatile dict;
|
||||||
|
|
||||||
|
@ -842,6 +842,13 @@ PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc)
|
||||||
if (attr->attisdropped)
|
if (attr->attisdropped)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (attr->attgenerated)
|
||||||
|
{
|
||||||
|
/* don't include unless requested */
|
||||||
|
if (!include_generated)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
key = NameStr(attr->attname);
|
key = NameStr(attr->attname);
|
||||||
vattr = heap_getattr(tuple, (i + 1), desc, &is_null);
|
vattr = heap_getattr(tuple, (i + 1), desc, &is_null);
|
||||||
|
|
||||||
|
|
|
@ -151,7 +151,7 @@ extern Datum PLy_output_convert(PLyObToDatum *arg, PyObject *val,
|
||||||
bool *isnull);
|
bool *isnull);
|
||||||
|
|
||||||
extern PyObject *PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple,
|
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,
|
extern void PLy_input_setup_func(PLyDatumToOb *arg, MemoryContext arg_mcxt,
|
||||||
Oid typeOid, int32 typmod,
|
Oid typeOid, int32 typmod,
|
||||||
|
|
|
@ -67,6 +67,11 @@ SELECT * FROM users;
|
||||||
CREATE TABLE trigger_test
|
CREATE TABLE trigger_test
|
||||||
(i int, v text );
|
(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 $$
|
CREATE FUNCTION trigger_data() RETURNS trigger LANGUAGE plpythonu AS $$
|
||||||
|
|
||||||
if 'relid' in TD:
|
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_before on trigger_test;
|
||||||
DROP TRIGGER show_trigger_data_trig_after 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');
|
insert into trigger_test values(1,'insert');
|
||||||
CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
|
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 TABLE transition_table_test;
|
||||||
DROP FUNCTION transition_table_test_f();
|
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;
|
||||||
|
|
|
@ -61,6 +61,10 @@ CREATE TABLE trigger_test (
|
||||||
);
|
);
|
||||||
-- Make certain dropped attributes are handled correctly
|
-- Make certain dropped attributes are handled correctly
|
||||||
ALTER TABLE trigger_test DROP dropme;
|
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 VIEW trigger_test_view AS SELECT i, v FROM trigger_test;
|
||||||
CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
|
CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
|
||||||
if {$TG_table_name eq "trigger_test" && $TG_level eq "ROW" && $TG_op ne "DELETE"} {
|
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
|
CREATE TRIGGER statement_trigger
|
||||||
BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON trigger_test
|
BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON trigger_test
|
||||||
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_data(42,'statement trigger');
|
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
|
CREATE TRIGGER show_trigger_data_view_trig
|
||||||
INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
|
INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
|
||||||
FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo 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_table_schema: public
|
||||||
NOTICE: TG_when: BEFORE
|
NOTICE: TG_when: BEFORE
|
||||||
NOTICE: args: {23 skidoo}
|
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');
|
insert into trigger_test_view values(2,'insert');
|
||||||
NOTICE: NEW: {i: 2, v: insert}
|
NOTICE: NEW: {i: 2, v: insert}
|
||||||
NOTICE: OLD: {}
|
NOTICE: OLD: {}
|
||||||
|
@ -738,6 +817,8 @@ NOTICE: TG_table_name: trigger_test
|
||||||
NOTICE: TG_table_schema: public
|
NOTICE: TG_table_schema: public
|
||||||
NOTICE: TG_when: BEFORE
|
NOTICE: TG_when: BEFORE
|
||||||
NOTICE: args: {42 {statement trigger}}
|
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
|
-- should error
|
||||||
insert into trigger_test(test_argisnull) values(true);
|
insert into trigger_test(test_argisnull) values(true);
|
||||||
NOTICE: NEW: {}
|
NOTICE: NEW: {}
|
||||||
|
@ -787,3 +868,21 @@ INFO: old: 1 -> a
|
||||||
INFO: new: 1 -> b
|
INFO: new: 1 -> b
|
||||||
drop table transition_table_test;
|
drop table transition_table_test;
|
||||||
drop function transition_table_test_f();
|
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)
|
||||||
|
|
||||||
|
|
|
@ -324,7 +324,7 @@ static void pltcl_subtrans_abort(Tcl_Interp *interp,
|
||||||
|
|
||||||
static void pltcl_set_tuple_values(Tcl_Interp *interp, const char *arrayname,
|
static void pltcl_set_tuple_values(Tcl_Interp *interp, const char *arrayname,
|
||||||
uint64 tupno, HeapTuple tuple, TupleDesc tupdesc);
|
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,
|
static HeapTuple pltcl_build_tuple_result(Tcl_Interp *interp,
|
||||||
Tcl_Obj **kvObjv, int kvObjc,
|
Tcl_Obj **kvObjv, int kvObjc,
|
||||||
pltcl_call_state *call_state);
|
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_len = HeapTupleHeaderGetDatumLength(td);
|
||||||
tmptup.t_data = 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);
|
Tcl_ListObjAppendElement(NULL, tcl_cmd, list_tmp);
|
||||||
|
|
||||||
ReleaseTupleDesc(tupdesc);
|
ReleaseTupleDesc(tupdesc);
|
||||||
|
@ -1060,7 +1060,6 @@ pltcl_trigger_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
|
||||||
volatile HeapTuple rettup;
|
volatile HeapTuple rettup;
|
||||||
Tcl_Obj *tcl_cmd;
|
Tcl_Obj *tcl_cmd;
|
||||||
Tcl_Obj *tcl_trigtup;
|
Tcl_Obj *tcl_trigtup;
|
||||||
Tcl_Obj *tcl_newtup;
|
|
||||||
int tcl_rc;
|
int tcl_rc;
|
||||||
int i;
|
int i;
|
||||||
const char *result;
|
const char *result;
|
||||||
|
@ -1162,20 +1161,22 @@ pltcl_trigger_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
|
||||||
Tcl_ListObjAppendElement(NULL, tcl_cmd,
|
Tcl_ListObjAppendElement(NULL, tcl_cmd,
|
||||||
Tcl_NewStringObj("ROW", -1));
|
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
|
* Now the command part of the event for TG_op and data for NEW
|
||||||
* and OLD
|
* 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))
|
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
|
||||||
{
|
{
|
||||||
Tcl_ListObjAppendElement(NULL, tcl_cmd,
|
Tcl_ListObjAppendElement(NULL, tcl_cmd,
|
||||||
Tcl_NewStringObj("INSERT", -1));
|
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());
|
Tcl_ListObjAppendElement(NULL, tcl_cmd, Tcl_NewObj());
|
||||||
|
|
||||||
rettup = trigdata->tg_trigtuple;
|
rettup = trigdata->tg_trigtuple;
|
||||||
|
@ -1186,7 +1187,10 @@ pltcl_trigger_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
|
||||||
Tcl_NewStringObj("DELETE", -1));
|
Tcl_NewStringObj("DELETE", -1));
|
||||||
|
|
||||||
Tcl_ListObjAppendElement(NULL, tcl_cmd, Tcl_NewObj());
|
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;
|
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_ListObjAppendElement(NULL, tcl_cmd,
|
||||||
Tcl_NewStringObj("UPDATE", -1));
|
Tcl_NewStringObj("UPDATE", -1));
|
||||||
|
|
||||||
tcl_newtup = pltcl_build_tuple_argument(trigdata->tg_newtuple,
|
Tcl_ListObjAppendElement(NULL, tcl_cmd,
|
||||||
tupdesc);
|
pltcl_build_tuple_argument(trigdata->tg_newtuple,
|
||||||
|
tupdesc,
|
||||||
Tcl_ListObjAppendElement(NULL, tcl_cmd, tcl_newtup);
|
!TRIGGER_FIRED_BEFORE(trigdata->tg_event)));
|
||||||
Tcl_ListObjAppendElement(NULL, tcl_cmd, tcl_trigtup);
|
Tcl_ListObjAppendElement(NULL, tcl_cmd,
|
||||||
|
pltcl_build_tuple_argument(trigdata->tg_trigtuple,
|
||||||
|
tupdesc,
|
||||||
|
true));
|
||||||
|
|
||||||
rettup = trigdata->tg_newtuple;
|
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
|
* from all attributes of a given tuple
|
||||||
**********************************************************************/
|
**********************************************************************/
|
||||||
static Tcl_Obj *
|
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();
|
Tcl_Obj *retobj = Tcl_NewObj();
|
||||||
int i;
|
int i;
|
||||||
|
@ -3110,6 +3117,13 @@ pltcl_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc)
|
||||||
if (att->attisdropped)
|
if (att->attisdropped)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (att->attgenerated)
|
||||||
|
{
|
||||||
|
/* don't include unless requested */
|
||||||
|
if (!include_generated)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
/************************************************************
|
/************************************************************
|
||||||
* Get the attribute name
|
* 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\"",
|
errmsg("cannot set system attribute \"%s\"",
|
||||||
fieldName)));
|
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]));
|
values[attn - 1] = utf_u2e(Tcl_GetString(kvObjv[i + 1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,11 @@ CREATE TABLE trigger_test (
|
||||||
-- Make certain dropped attributes are handled correctly
|
-- Make certain dropped attributes are handled correctly
|
||||||
ALTER TABLE trigger_test DROP dropme;
|
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 VIEW trigger_test_view AS SELECT i, v FROM trigger_test;
|
||||||
|
|
||||||
CREATE FUNCTION trigger_data() returns trigger language pltcl as $_$
|
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
|
BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON trigger_test
|
||||||
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_data(42,'statement trigger');
|
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
|
CREATE TRIGGER show_trigger_data_view_trig
|
||||||
INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
|
INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
|
||||||
FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo 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
|
-- show dump of trigger data
|
||||||
insert into trigger_test values(1,'insert');
|
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');
|
insert into trigger_test_view values(2,'insert');
|
||||||
update trigger_test_view set v = 'update' where i=1;
|
update trigger_test_view set v = 'update' where i=1;
|
||||||
delete from trigger_test_view;
|
delete from trigger_test_view;
|
||||||
|
@ -540,6 +556,9 @@ update trigger_test set v = 'update' where i = 1;
|
||||||
delete from trigger_test;
|
delete from trigger_test;
|
||||||
truncate 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
|
-- should error
|
||||||
insert into trigger_test(test_argisnull) values(true);
|
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';
|
update transition_table_test set name = 'b';
|
||||||
drop table transition_table_test;
|
drop table transition_table_test;
|
||||||
drop function transition_table_test_f();
|
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;
|
||||||
|
|
|
@ -113,6 +113,52 @@ SELECT * FROM test_like_id_3; -- identity was copied and applied
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
DROP TABLE test_like_id_1, test_like_id_2, test_like_id_3;
|
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 */
|
CREATE TABLE inhg (x text, LIKE inhx INCLUDING INDEXES, y text); /* copies indexes */
|
||||||
INSERT INTO inhg VALUES (5, 10);
|
INSERT INTO inhg VALUES (5, 10);
|
||||||
INSERT INTO inhg VALUES (20, 10); -- should fail
|
INSERT INTO inhg VALUES (20, 10); -- should fail
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -84,7 +84,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
|
||||||
# ----------
|
# ----------
|
||||||
# Another group of parallel tests
|
# 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
|
# Another group of parallel tests
|
||||||
|
|
|
@ -122,6 +122,7 @@ test: groupingsets
|
||||||
test: drop_operator
|
test: drop_operator
|
||||||
test: password
|
test: password
|
||||||
test: identity
|
test: identity
|
||||||
|
test: generated
|
||||||
test: create_table_like
|
test: create_table_like
|
||||||
test: alter_generic
|
test: alter_generic
|
||||||
test: alter_operator
|
test: alter_operator
|
||||||
|
|
|
@ -51,6 +51,20 @@ INSERT INTO test_like_id_3 (b) VALUES ('b3');
|
||||||
SELECT * FROM test_like_id_3; -- identity was copied and applied
|
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;
|
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 */
|
CREATE TABLE inhg (x text, LIKE inhx INCLUDING INDEXES, y text); /* copies indexes */
|
||||||
INSERT INTO inhg VALUES (5, 10);
|
INSERT INTO inhg VALUES (5, 10);
|
||||||
INSERT INTO inhg VALUES (20, 10); -- should fail
|
INSERT INTO inhg VALUES (20, 10); -- should fail
|
||||||
|
|
|
@ -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*
|
|
@ -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');
|
Loading…
Reference in New Issue