CREATE TABLE LIKE INCLUDING TRIGGERS
hi.
poc demo:
CREATE TABLE main_table (a int, b int);
CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS '
BEGIN
RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'',
TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
RETURN NULL;
END;';
CREATE TRIGGER modified_a BEFORE UPDATE OF a ON main_table
FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE trigger_func('modified_a');
CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS INCLUDING COMMENTS);
\d main_table1
Table "public.main_table1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | |
b | integer | | |
Triggers:
modified_a BEFORE UPDATE OF a ON main_table1 FOR EACH ROW WHEN
(old.a <> new.a) EXECUTE FUNCTION trigger_func('modified_a')
foreign key associated internal triggers won't be copied to the new table.
source table trigger associated comment will be copied to the new table,
if INCLUDING COMMENTS is specified.
---------------
v1-0001: "refactor CreateTrigger and CreateTriggerFiringOn".
Similar to CreateStatistics, some of the expressions stored in the
catalog pg_trigger are
already transformed, when we retrieve it as a base model for constructing a new
CreateTrigStmt, we can not do parse analysis of it again.
see transformStatsStmt for similar handling.
The CreateTrigger function, (Node *whenClause) is always NULL,
so I think it's safe to remove the argument whenClause.
v1-0002: CREATE TABLE LIKE INCLUDING TRIGGERS.
Attachments:
v1-0002-CREATE-TABLE-LIKE-INCLUDING-TRIGGERS.patchtext/x-patch; charset=US-ASCII; name=v1-0002-CREATE-TABLE-LIKE-INCLUDING-TRIGGERS.patchDownload
From 760b83bd561eff3447bf07e74fc1fb4e494a7e26 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 29 Sep 2025 12:33:25 +0800
Subject: [PATCH v1 2/2] CREATE TABLE LIKE INCLUDING TRIGGERS
foreign key associated internal trigger won't being copied to new table. source
table trigger associated comment will copied to new table, if INCLUDING COMMENTS
is specified.
Currently not support CREATE FOREIGN TABLE LIKE. (maybe we should).
discussion: https://postgr.es/m/
---
doc/src/sgml/ref/create_table.sgml | 13 +-
src/backend/catalog/index.c | 1 +
src/backend/commands/tablecmds.c | 4 +
src/backend/commands/trigger.c | 6 +
src/backend/parser/gram.y | 7 +-
src/backend/parser/parse_utilcmd.c | 244 ++++++++++++++++++++++++-
src/include/nodes/parsenodes.h | 2 +
src/include/parser/kwlist.h | 1 +
src/test/regress/expected/triggers.out | 99 ++++++++++
src/test/regress/sql/triggers.sql | 43 +++++
10 files changed, 416 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index dc000e913c1..b1b33864ae7 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -88,7 +88,7 @@ class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable cl
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | TRIGGERS | ALL }
<phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
@@ -672,7 +672,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>INCLUDING COMMENTS</literal></term>
<listitem>
<para>
- Comments for the copied columns, constraints, and indexes will be
+ Comments for the copied columns, constraints, indexes and triggers will be
copied. The default behavior is to exclude comments, resulting in
the copied columns and constraints in the new table having no
comments.
@@ -776,6 +776,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-like-opt-triggers">
+ <term><literal>INCLUDING TRIGGERS</literal></term>
+ <listitem>
+ <para>
+ Any non-internal triggers on the original table will be created on the new table.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-parms-like-opt-all">
<term><literal>INCLUDING ALL</literal></term>
<listitem>
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 8d09d61cae2..5c0df73c1c1 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2040,6 +2040,7 @@ index_constraint_create(Relation heapRelation,
trigger->initdeferred = initdeferred;
trigger->constrrel = NULL;
trigger->transformed = true;
+ trigger->trigcomment = NULL;
(void) CreateTrigger(trigger, NULL, RelationGetRelid(heapRelation),
InvalidOid, conOid, indexRelationId, InvalidOid,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index bf8120c12f2..002b85c3d4b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13802,6 +13802,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->deferrable = fkconstraint->deferrable;
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
+ fk_trigger->trigcomment = NULL;
fk_trigger->transformed = true;
trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
@@ -13848,6 +13849,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
+ fk_trigger->trigcomment = NULL;
fk_trigger->transformed = true;
switch (fkconstraint->fk_del_action)
@@ -13909,6 +13911,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
+ fk_trigger->trigcomment = NULL;
fk_trigger->transformed = true;
switch (fkconstraint->fk_upd_action)
@@ -20831,6 +20834,7 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
trigStmt->deferrable = trigForm->tgdeferrable;
trigStmt->initdeferred = trigForm->tginitdeferred;
trigStmt->constrrel = NULL; /* passed separately */
+ trigStmt->trigcomment = NULL;
trigStmt->transformed = true; /* whenClause alerady transformed */
CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition),
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index f1431d99a56..324f31fc501 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -30,6 +30,7 @@
#include "catalog/pg_proc.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
+#include "commands/comment.h"
#include "commands/trigger.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -1122,6 +1123,11 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
/* Keep lock on target rel until end of xact */
table_close(rel, NoLock);
+ /* Add any requested comment */
+ if (stmt->trigcomment != NULL)
+ CreateComments(trigoid, TriggerRelationId, 0,
+ stmt->trigcomment);
+
return myself;
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 05ff0c5973f..b59b2eef2c7 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -776,7 +776,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
TABLE TABLES TABLESAMPLE TABLESPACE TARGET TEMP TEMPLATE TEMPORARY TEXT_P THEN
TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSFORM
- TREAT TRIGGER TRIM TRUE_P
+ TREAT TRIGGER TRIGGERS TRIM TRUE_P
TRUNCATE TRUSTED TYPE_P TYPES_P
UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN
@@ -4216,6 +4216,7 @@ TableLikeOption:
| INDEXES { $$ = CREATE_TABLE_LIKE_INDEXES; }
| STATISTICS { $$ = CREATE_TABLE_LIKE_STATISTICS; }
| STORAGE { $$ = CREATE_TABLE_LIKE_STORAGE; }
+ | TRIGGERS { $$ = CREATE_TABLE_LIKE_TRIGGERS; }
| ALL { $$ = CREATE_TABLE_LIKE_ALL; }
;
@@ -6070,6 +6071,7 @@ CreateTrigStmt:
n->deferrable = false;
n->initdeferred = false;
n->constrrel = NULL;
+ n->trigcomment = NULL;
n->transformed = false;
$$ = (Node *) n;
}
@@ -6121,6 +6123,7 @@ CreateTrigStmt:
&n->deferrable, &n->initdeferred, &dummy,
NULL, NULL, yyscanner);
n->constrrel = $10;
+ n->trigcomment = NULL;
n->transformed = false;
$$ = (Node *) n;
}
@@ -18028,6 +18031,7 @@ unreserved_keyword:
| TRANSACTION
| TRANSFORM
| TRIGGER
+ | TRIGGERS
| TRUNCATE
| TRUSTED
| TYPE_P
@@ -18677,6 +18681,7 @@ bare_label_keyword:
| TRANSFORM
| TREAT
| TRIGGER
+ | TRIGGERS
| TRIM
| TRUE_P
| TRUNCATE
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index fd6d5c922ec..7b8cccc4018 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -37,6 +37,7 @@
#include "catalog/pg_constraint.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
+#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -62,6 +63,7 @@
#include "rewrite/rewriteManip.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/partcache.h"
#include "utils/rel.h"
@@ -121,6 +123,11 @@ static CreateStatsStmt *generateClonedExtStatsStmt(RangeVar *heapRel,
Oid heapRelid,
Oid source_statsid,
const AttrMap *attmap);
+static CreateTrigStmt *generateClonedTriggerStmt(RangeVar *heapRel,
+ Oid source_trigid,
+ Relation source_rel,
+ const AttrMap *attmap);
+
static List *get_collation(Oid collation, Oid actual_datatype);
static List *get_opclass(Oid opclass, Oid actual_datatype);
static void transformIndexConstraints(CreateStmtContext *cxt);
@@ -1319,7 +1326,8 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
CREATE_TABLE_LIKE_GENERATED |
CREATE_TABLE_LIKE_CONSTRAINTS |
CREATE_TABLE_LIKE_INDEXES |
- CREATE_TABLE_LIKE_STATISTICS))
+ CREATE_TABLE_LIKE_STATISTICS |
+ CREATE_TABLE_LIKE_TRIGGERS))
{
table_like_clause->relationOid = RelationGetRelid(relation);
cxt->likeclauses = lappend(cxt->likeclauses, table_like_clause);
@@ -1585,6 +1593,46 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
}
}
+ /* Process triggers if required */
+ if ((table_like_clause->options & CREATE_TABLE_LIKE_TRIGGERS) &&
+ relation->trigdesc != NULL &&
+ childrel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
+ {
+ bool include_comments;
+
+ include_comments = (table_like_clause->options & CREATE_TABLE_LIKE_COMMENTS);
+
+ for (int nt = 0; nt < relation->trigdesc->numtriggers; nt++)
+ {
+ Trigger *trig;
+ Oid trigoid = InvalidOid;
+ CreateTrigStmt *trig_stmt = NULL;
+
+ trig = relation->trigdesc->triggers + nt;
+ trigoid = trig->tgoid;
+
+ if (trig->tgisinternal)
+ continue;
+
+ /* internal trigger won't copied to new table */
+ trig_stmt = generateClonedTriggerStmt(heapRel,
+ trigoid,
+ relation,
+ attmap);
+
+ /* Copy comment on trigger, if requested */
+ if (include_comments)
+ {
+ comment = GetComment(trigoid, TriggerRelationId, 0);
+
+ /* We make use of CreateTrigStmt's trigcomment option */
+ trig_stmt->trigcomment = comment;
+ }
+
+ result = lappend(result, trig_stmt);
+ }
+ }
+
/*
* Process extended statistics if required.
*/
@@ -2164,6 +2212,200 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid,
return stats;
}
+/*
+ * Generate a CreateTrigStmt node using information from an already existing
+ * trigger "source_trigid" on source_rel, for the rel identified by heapRel.
+ * Ensure source trigger is not internal trigger.
+ *
+ * Attribute numbers in expression Vars are adjusted according to attmap.
+ */
+static CreateTrigStmt *
+generateClonedTriggerStmt(RangeVar *heapRel, Oid source_trigid,
+ Relation source_rel, const AttrMap *attmap)
+{
+ CreateTrigStmt *trigStmt;
+ HeapTuple triggerTuple;
+ HeapTuple proctup;
+ Form_pg_trigger trigForm;
+ Form_pg_proc procform;
+ Relation pg_trigger;
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Datum value;
+ bool isnull;
+ Node *qual = NULL;
+ List *trigargs = NIL;
+ List *cols = NIL;
+ List *funcname = NIL;
+ List *transitionRels = NIL;
+ char *schemaname;
+
+ pg_trigger = table_open(TriggerRelationId, AccessShareLock);
+
+ /* Find the trigger to copy */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(source_trigid));
+
+ tgscan = systable_beginscan(pg_trigger, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ triggerTuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(triggerTuple))
+ elog(ERROR, "could not find tuple for trigger %u", source_trigid);
+
+ trigForm = (Form_pg_trigger) GETSTRUCT(triggerTuple);
+
+ Assert(!trigForm->tgisinternal);
+
+ /* Reconstruct trigger function String list */
+ proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(trigForm->tgfoid));
+ if (!HeapTupleIsValid(proctup))
+ elog(ERROR, "cache lookup failed for function %u", trigForm->tgfoid);
+ procform = (Form_pg_proc) GETSTRUCT(proctup);
+
+ schemaname = get_namespace_name(procform->pronamespace);
+ funcname = list_make2(makeString(schemaname),
+ makeString(NameStr(procform->proname)));
+ ReleaseSysCache(proctup);
+
+ /* Reconstruct trigger arguments list */
+ if (trigForm->tgnargs > 0)
+ {
+ char *p;
+
+ value = heap_getattr(triggerTuple, Anum_pg_trigger_tgargs,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (isnull)
+ elog(ERROR, "tgargs is null for trigger \"%s\" in relation \"%s\"",
+ NameStr(trigForm->tgname),
+ RelationGetRelationName(source_rel));
+
+ p = (char *) VARDATA_ANY(DatumGetByteaPP(value));
+
+ for (int i = 0; i < trigForm->tgnargs; i++)
+ {
+ trigargs = lappend(trigargs, makeString(pstrdup(p)));
+ p += strlen(p) + 1;
+ }
+ }
+
+ /*
+ * If there is a column list, transform it to a list of column names.
+ * Note we don't need to map this list in any way ...
+ */
+ if (trigForm->tgattr.dim1 > 0)
+ {
+ int i;
+
+ for (i = 0; i < trigForm->tgattr.dim1; i++)
+ {
+ Form_pg_attribute col;
+
+ col = TupleDescAttr(RelationGetDescr(source_rel),
+ trigForm->tgattr.values[i] - 1);
+ cols = lappend(cols,
+ makeString(pstrdup(NameStr(col->attname))));
+ }
+ }
+
+ /* If the trigger has a WHEN qualification, add that */
+ value = fastgetattr(triggerTuple, Anum_pg_trigger_tgqual,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (!isnull)
+ {
+ bool found_whole_row;
+
+ qual = stringToNode(TextDatumGetCString(value));
+
+ /* Adjust Vars to match new table's column numbering */
+ qual = map_variable_attnos(qual, PRS2_NEW_VARNO, 0,
+ attmap,
+ InvalidOid, &found_whole_row);
+
+ /* As in expandTableLikeClause, reject whole-row variables */
+ if (found_whole_row)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot convert whole-row table reference"),
+ errdetail("Trigger \"%s\" contains a whole-row table reference.",
+ NameStr(trigForm->tgname)));
+
+ qual = map_variable_attnos(qual, PRS2_OLD_VARNO, 0,
+ attmap,
+ InvalidOid, &found_whole_row);
+
+ /* As in expandTableLikeClause, reject whole-row variables */
+ if (found_whole_row)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot convert whole-row table reference"),
+ errdetail("Trigger \"%s\" contains a whole-row table reference.",
+ NameStr(trigForm->tgname)));
+ }
+
+ /* Reconstruct trigger old transition table */
+ value = fastgetattr(triggerTuple, Anum_pg_trigger_tgoldtable,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (!isnull)
+ {
+ TriggerTransition *old = makeNode(TriggerTransition);
+ old->isNew = false;
+ old->name = NameStr(*DatumGetName(value));
+ old->isTable = true;
+ transitionRels = lappend(transitionRels, old);
+ }
+
+ /* Reconstruct trigger old transition table */
+ value = fastgetattr(triggerTuple, Anum_pg_trigger_tgnewtable,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (!isnull)
+ {
+ TriggerTransition *new = makeNode(TriggerTransition);
+ new->isNew = true;
+ new->name = NameStr(*DatumGetName(value));
+ new->isTable = true;
+ transitionRels = lappend(transitionRels, new);
+ }
+
+ trigStmt = makeNode(CreateTrigStmt);
+ trigStmt->replace = false;
+ trigStmt->isconstraint = OidIsValid(trigForm->tgconstraint);
+ trigStmt->trigname = NameStr(trigForm->tgname);
+ trigStmt->relation = heapRel;
+ trigStmt->funcname = funcname;
+ trigStmt->args = trigargs;
+ trigStmt->row = TRIGGER_FOR_ROW(trigForm->tgtype);
+ trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK;
+ trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK;
+ trigStmt->columns = cols;
+ trigStmt->whenClause = qual;
+ trigStmt->transitionRels = transitionRels;
+ trigStmt->deferrable = trigForm->tgdeferrable;
+ trigStmt->initdeferred = trigForm->tginitdeferred;
+
+ trigStmt->constrrel = NULL;
+ if (OidIsValid(trigForm->tgconstrrelid))
+ {
+ const char *relname;
+ const char *nspname;
+ Oid nspoid;
+
+ relname = quote_identifier(get_rel_name(trigForm->tgconstrrelid));
+ nspoid = get_rel_namespace(trigForm->tgconstrrelid);
+ nspname = get_namespace_name_or_temp(nspoid);
+ nspname = quote_identifier(nspname);
+ trigStmt->constrrel = makeRangeVar((char *) nspname, (char *) relname, -1);
+ }
+ trigStmt->transformed = true; /* don't need transformCreateTriggerStmt again */
+
+ systable_endscan(tgscan);
+ table_close(pg_trigger, AccessShareLock);
+
+ return trigStmt;
+}
+
/*
* get_collation - fetch qualified name of a collation
*
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9d760e15145..7ecbeb64285 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -793,6 +793,7 @@ typedef enum TableLikeOption
CREATE_TABLE_LIKE_INDEXES = 1 << 6,
CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+ CREATE_TABLE_LIKE_TRIGGERS = 1 << 9,
CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
} TableLikeOption;
@@ -3122,6 +3123,7 @@ typedef struct CreateTrigStmt
bool deferrable; /* [NOT] DEFERRABLE */
bool initdeferred; /* INITIALLY {DEFERRED|IMMEDIATE} */
RangeVar *constrrel; /* opposite relation, if RI trigger */
+ char *trigcomment; /* comment to apply to trigger, or NULL */
bool transformed; /* true when transformCreateTriggerStmt is finished */
} CreateTrigStmt;
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index a4af3f717a1..74cac2779f5 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -459,6 +459,7 @@ PG_KEYWORD("transaction", TRANSACTION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("transform", TRANSFORM, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("treat", TREAT, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("trigger", TRIGGER, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("triggers", TRIGGERS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("trim", TRIM, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("true", TRUE_P, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("truncate", TRUNCATE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 1eb8fba0953..3c578b09ce3 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -174,6 +174,16 @@ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_table
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt');
CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt');
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW WHEN (OLD is not null) EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+ERROR: cannot convert whole-row table reference
+DETAIL: Trigger "wholetrig" contains a whole-row table reference.
+DROP TRIGGER wholetrig ON main_table;
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW WHEN (NEW is not null) EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+ERROR: cannot convert whole-row table reference
+DETAIL: Trigger "wholetrig" contains a whole-row table reference.
+DROP TRIGGER wholetrig ON main_table;
--
-- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified,
-- CREATE TRIGGER should default to 'FOR EACH STATEMENT'
@@ -394,6 +404,50 @@ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER,
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+--create table like tests
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS 'trigger before_ins_stmt_trig';
+CREATE TABLE main_table1(c INT, LIKE main_table INCLUDING TRIGGERS INCLUDING COMMENTS);
+\d main_table
+ Table "public.main_table"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ after_ins_stmt_trig AFTER INSERT ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_ins_stmt')
+ after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_a_b_row')
+ after_upd_b_row_trig AFTER UPDATE OF b ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_b_row')
+ after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_b_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_ins_stmt')
+ before_upd_a_row_trig BEFORE UPDATE OF a ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('before_upd_a_row')
+ before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_upd_a_stmt')
+
+\d main_table1
+ Table "public.main_table1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ c | integer | | |
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ after_ins_stmt_trig AFTER INSERT ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_ins_stmt')
+ after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table1 FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_a_b_row')
+ after_upd_b_row_trig AFTER UPDATE OF b ON main_table1 FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_b_row')
+ after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_b_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_ins_stmt')
+ before_upd_a_row_trig BEFORE UPDATE OF a ON main_table1 FOR EACH ROW EXECUTE FUNCTION trigger_func('before_upd_a_row')
+ before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_upd_a_stmt')
+
+\dd before_ins_stmt_trig
+ Object descriptions
+ Schema | Name | Object | Description
+--------+----------------------+---------+------------------------------
+ public | before_ins_stmt_trig | trigger | trigger before_ins_stmt_trig
+(1 row)
+
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS NULL;
--
-- Test case for bug with BEFORE trigger followed by AFTER trigger with WHEN
--
@@ -861,6 +915,9 @@ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+CREATE TABLE main_view_table(LIKE main_view INCLUDING TRIGGERS); --error
+ERROR: "main_view_table" is a table
+DETAIL: Tables cannot have INSTEAD OF triggers.
-- Valid BEFORE statement VIEW triggers
CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
@@ -2318,6 +2375,18 @@ create constraint trigger parted_trig_two after insert on parted_constr
deferrable initially deferred enforced
for each row when (bark(new.b) AND new.a % 2 = 1)
execute procedure trigger_notice_ab();
+create table parted_constr_copy (like parted_constr including all);
+select pg_get_triggerdef(oid)
+from pg_trigger
+where not tgisinternal and tgrelid = 'parted_constr_copy'::regclass
+order by tgname;
+ pg_get_triggerdef
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE CONSTRAINT TRIGGER parted_trig AFTER INSERT ON public.parted_constr_copy DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE FUNCTION trigger_notice_ab()
+ CREATE CONSTRAINT TRIGGER parted_trig_two AFTER INSERT ON public.parted_constr_copy DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN ((bark(new.b) AND ((new.a % 2) = 1))) EXECUTE FUNCTION trigger_notice_ab()
+(2 rows)
+
+drop table parted_constr_copy;
-- The immediate constraint is fired immediately; the WHEN clause of the
-- deferred constraint is also called immediately. The deferred constraint
-- is fired at commit time.
@@ -3092,6 +3161,18 @@ create trigger iocdu_tt_parted_insert_trig
create trigger iocdu_tt_parted_update_trig
after update on iocdu_tt_parted referencing old table as old_table new table as new_table
for each statement execute procedure dump_update();
+CREATE TABLE iocdu_tt_parted_copy(LIKE iocdu_tt_parted INCLUDING TRIGGERS);
+\d iocdu_tt_parted_copy
+ Table "public.iocdu_tt_parted_copy"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | not null |
+ b | text | | |
+Triggers:
+ iocdu_tt_parted_insert_trig AFTER INSERT ON iocdu_tt_parted_copy REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION dump_insert()
+ iocdu_tt_parted_update_trig AFTER UPDATE ON iocdu_tt_parted_copy REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION dump_update()
+
+DROP TABLE iocdu_tt_parted_copy;
-- inserts only
insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB')
on conflict (a) do
@@ -3574,6 +3655,24 @@ begin
end;
$$;
alter function whoami() owner to regress_fn_owner;
+--CREATE TABLE LIKE INCLUDING TRIGGERS
+-- test constraint trigger that reference another table
+create table t1 (id integer);
+create table t2 (id integer);
+create constraint trigger con_trig_test after insert on t1 from t2
+ deferrable initially deferred
+ for each row
+ execute function whoami();
+create table t1_copy(like t1 including triggers);
+\d t1_copy
+ Table "public.t1_copy"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | |
+Triggers:
+ con_trig_test AFTER INSERT ON t1_copy FROM t2 DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION whoami()
+
+drop table t1, t2, t1_copy;
create table defer_trig (id integer);
grant insert on defer_trig to public;
create constraint trigger whoami after insert on defer_trig
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 5f7f75d7ba5..a58edc06c9b 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -120,6 +120,14 @@ FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt');
CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt');
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW WHEN (OLD is not null) EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+DROP TRIGGER wholetrig ON main_table;
+
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW WHEN (NEW is not null) EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+DROP TRIGGER wholetrig ON main_table;
+
--
-- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified,
-- CREATE TRIGGER should default to 'FOR EACH STATEMENT'
@@ -234,6 +242,15 @@ SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = 'main_table'::regc
UPDATE main_table SET a = 50;
UPDATE main_table SET b = 10;
+--create table like tests
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS 'trigger before_ins_stmt_trig';
+CREATE TABLE main_table1(c INT, LIKE main_table INCLUDING TRIGGERS INCLUDING COMMENTS);
+\d main_table
+\d main_table1
+
+\dd before_ins_stmt_trig
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS NULL;
+
--
-- Test case for bug with BEFORE trigger followed by AFTER trigger with WHEN
--
@@ -621,6 +638,8 @@ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+CREATE TABLE main_view_table(LIKE main_view INCLUDING TRIGGERS); --error
+
-- Valid BEFORE statement VIEW triggers
CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
@@ -1608,6 +1627,14 @@ create constraint trigger parted_trig_two after insert on parted_constr
for each row when (bark(new.b) AND new.a % 2 = 1)
execute procedure trigger_notice_ab();
+create table parted_constr_copy (like parted_constr including all);
+select pg_get_triggerdef(oid)
+from pg_trigger
+where not tgisinternal and tgrelid = 'parted_constr_copy'::regclass
+order by tgname;
+
+drop table parted_constr_copy;
+
-- The immediate constraint is fired immediately; the WHEN clause of the
-- deferred constraint is also called immediately. The deferred constraint
-- is fired at commit time.
@@ -2278,6 +2305,10 @@ create trigger iocdu_tt_parted_update_trig
after update on iocdu_tt_parted referencing old table as old_table new table as new_table
for each statement execute procedure dump_update();
+CREATE TABLE iocdu_tt_parted_copy(LIKE iocdu_tt_parted INCLUDING TRIGGERS);
+\d iocdu_tt_parted_copy
+DROP TABLE iocdu_tt_parted_copy;
+
-- inserts only
insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB')
on conflict (a) do
@@ -2735,6 +2766,18 @@ end;
$$;
alter function whoami() owner to regress_fn_owner;
+--CREATE TABLE LIKE INCLUDING TRIGGERS
+-- test constraint trigger that reference another table
+create table t1 (id integer);
+create table t2 (id integer);
+create constraint trigger con_trig_test after insert on t1 from t2
+ deferrable initially deferred
+ for each row
+ execute function whoami();
+create table t1_copy(like t1 including triggers);
+\d t1_copy
+drop table t1, t2, t1_copy;
+
create table defer_trig (id integer);
grant insert on defer_trig to public;
create constraint trigger whoami after insert on defer_trig
--
2.34.1
v1-0001-refactor-CreateTrigger-and-CreateTriggerFiringOn.patchtext/x-patch; charset=US-ASCII; name=v1-0001-refactor-CreateTrigger-and-CreateTriggerFiringOn.patchDownload
From d5e784ad3cbb88aabd2e1f68dabc3ca599508945 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Thu, 25 Sep 2025 16:41:36 +0800
Subject: [PATCH v1 1/2] refactor CreateTrigger and CreateTriggerFiringOn
discussion: https://postgr.es/m/
---
src/backend/catalog/index.c | 3 +-
src/backend/commands/tablecmds.c | 14 ++-
src/backend/commands/trigger.c | 180 ++++++++---------------------
src/backend/parser/gram.y | 2 +
src/backend/parser/parse_utilcmd.c | 144 +++++++++++++++++++++++
src/backend/tcop/utility.c | 2 +-
src/include/commands/trigger.h | 4 +-
src/include/nodes/parsenodes.h | 1 +
src/include/parser/parse_utilcmd.h | 2 +
9 files changed, 212 insertions(+), 140 deletions(-)
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 5d9db167e59..8d09d61cae2 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2039,10 +2039,11 @@ index_constraint_create(Relation heapRelation,
trigger->deferrable = true;
trigger->initdeferred = initdeferred;
trigger->constrrel = NULL;
+ trigger->transformed = true;
(void) CreateTrigger(trigger, NULL, RelationGetRelid(heapRelation),
InvalidOid, conOid, indexRelationId, InvalidOid,
- InvalidOid, NULL, true, false);
+ InvalidOid, true, false);
}
/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fc89352b661..bf8120c12f2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13802,10 +13802,11 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->deferrable = fkconstraint->deferrable;
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
+ fk_trigger->transformed = true;
trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
constraintOid, indexOid, InvalidOid,
- parentTrigOid, NULL, true, false);
+ parentTrigOid, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -13847,6 +13848,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
+ fk_trigger->transformed = true;
switch (fkconstraint->fk_del_action)
{
@@ -13883,7 +13885,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
- parentDelTrigger, NULL, true, false);
+ parentDelTrigger, true, false);
if (deleteTrigOid)
*deleteTrigOid = trigAddress.objectId;
@@ -13907,6 +13909,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
+ fk_trigger->transformed = true;
switch (fkconstraint->fk_upd_action)
{
@@ -13943,7 +13946,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
- parentUpdTrigger, NULL, true, false);
+ parentUpdTrigger, true, false);
if (updateTrigOid)
*updateTrigOid = trigAddress.objectId;
}
@@ -20823,15 +20826,16 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK;
trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK;
trigStmt->columns = cols;
- trigStmt->whenClause = NULL; /* passed separately */
+ trigStmt->whenClause = qual;
trigStmt->transitionRels = NIL; /* not supported at present */
trigStmt->deferrable = trigForm->tgdeferrable;
trigStmt->initdeferred = trigForm->tginitdeferred;
trigStmt->constrrel = NULL; /* passed separately */
+ trigStmt->transformed = true; /* whenClause alerady transformed */
CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition),
trigForm->tgconstrrelid, InvalidOid, InvalidOid,
- trigForm->tgfoid, trigForm->oid, qual,
+ trigForm->tgfoid, trigForm->oid,
false, true, trigForm->tgenabled);
MemoryContextSwitchTo(oldcxt);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 579ac8d76ae..f1431d99a56 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -40,6 +40,7 @@
#include "parser/parse_collate.h"
#include "parser/parse_func.h"
#include "parser/parse_relation.h"
+#include "parser/parse_utilcmd.h"
#include "partitioning/partdesc.h"
#include "pgstat.h"
#include "rewrite/rewriteHandler.h"
@@ -139,9 +140,6 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
* create the trigger on partitions, 2) when creating child foreign key
* triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
- * If whenClause is passed, it is an already-transformed expression for
- * WHEN. In this case, we ignore any that may come in stmt->whenClause.
- *
* If isInternal is true then this is an internally-generated trigger.
* This argument sets the tgisinternal field of the pg_trigger entry, and
* if true causes us to modify the given trigger name to ensure uniqueness.
@@ -159,13 +157,13 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
ObjectAddress
CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
- Oid funcoid, Oid parentTriggerOid, Node *whenClause,
+ Oid funcoid, Oid parentTriggerOid,
bool isInternal, bool in_partition)
{
return
CreateTriggerFiringOn(stmt, queryString, relOid, refRelOid,
constraintOid, indexOid, funcoid,
- parentTriggerOid, whenClause, isInternal,
+ parentTriggerOid, isInternal,
in_partition, TRIGGER_FIRES_ON_ORIGIN);
}
@@ -177,15 +175,15 @@ ObjectAddress
CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
Oid relOid, Oid refRelOid, Oid constraintOid,
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
- Node *whenClause, bool isInternal, bool in_partition,
+ bool isInternal, bool in_partition,
char trigger_fires_when)
{
int16 tgtype;
int ncolumns;
int16 *columns;
int2vector *tgattr;
- List *whenRtable;
- char *qual;
+ List *whenRtable = NIL;
+ char *qual = NULL;
Datum values[Natts_pg_trigger];
bool nulls[Natts_pg_trigger];
Relation rel;
@@ -207,6 +205,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
bool existing_isClone = false;
+ Node *whenClause = NULL;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -557,133 +556,21 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
/*
* Parse the WHEN clause, if any and we weren't passed an already
* transformed one.
- *
- * Note that as a side effect, we fill whenRtable when parsing. If we got
- * an already parsed clause, this does not occur, which is what we want --
- * no point in adding redundant dependencies below.
*/
- if (!whenClause && stmt->whenClause)
+ if (stmt->whenClause)
{
- ParseState *pstate;
- ParseNamespaceItem *nsitem;
- List *varList;
- ListCell *lc;
-
- /* Set up a pstate to parse with */
- pstate = make_parsestate(NULL);
- pstate->p_sourcetext = queryString;
-
- /*
- * Set up nsitems for OLD and NEW references.
- *
- * 'OLD' must always have varno equal to 1 and 'NEW' equal to 2.
- */
- nsitem = addRangeTableEntryForRelation(pstate, rel,
- AccessShareLock,
- makeAlias("old", NIL),
- false, false);
- addNSItemToQuery(pstate, nsitem, false, true, true);
- nsitem = addRangeTableEntryForRelation(pstate, rel,
- AccessShareLock,
- makeAlias("new", NIL),
- false, false);
- addNSItemToQuery(pstate, nsitem, false, true, true);
-
- /* Transform expression. Copy to be sure we don't modify original */
- whenClause = transformWhereClause(pstate,
- copyObject(stmt->whenClause),
- EXPR_KIND_TRIGGER_WHEN,
- "WHEN");
- /* we have to fix its collations too */
- assign_expr_collations(pstate, whenClause);
-
- /*
- * Check for disallowed references to OLD/NEW.
- *
- * NB: pull_var_clause is okay here only because we don't allow
- * subselects in WHEN clauses; it would fail to examine the contents
- * of subselects.
- */
- varList = pull_var_clause(whenClause, 0);
- foreach(lc, varList)
+ if (!stmt->transformed)
{
- Var *var = (Var *) lfirst(lc);
+ stmt = transformCreateTriggerStmt(RelationGetRelid(rel), stmt,
+ queryString);
- switch (var->varno)
- {
- case PRS2_OLD_VARNO:
- if (!TRIGGER_FOR_ROW(tgtype))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("statement trigger's WHEN condition cannot reference column values"),
- parser_errposition(pstate, var->location)));
- if (TRIGGER_FOR_INSERT(tgtype))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("INSERT trigger's WHEN condition cannot reference OLD values"),
- parser_errposition(pstate, var->location)));
- /* system columns are okay here */
- break;
- case PRS2_NEW_VARNO:
- if (!TRIGGER_FOR_ROW(tgtype))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("statement trigger's WHEN condition cannot reference column values"),
- parser_errposition(pstate, var->location)));
- if (TRIGGER_FOR_DELETE(tgtype))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("DELETE trigger's WHEN condition cannot reference NEW values"),
- parser_errposition(pstate, var->location)));
- if (var->varattno < 0 && TRIGGER_FOR_BEFORE(tgtype))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("BEFORE trigger's WHEN condition cannot reference NEW system columns"),
- parser_errposition(pstate, var->location)));
- if (TRIGGER_FOR_BEFORE(tgtype) &&
- var->varattno == 0 &&
- RelationGetDescr(rel)->constr &&
- (RelationGetDescr(rel)->constr->has_generated_stored ||
- RelationGetDescr(rel)->constr->has_generated_virtual))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
- errdetail("A whole-row reference is used and the table contains generated columns."),
- parser_errposition(pstate, var->location)));
- if (TRIGGER_FOR_BEFORE(tgtype) &&
- var->varattno > 0 &&
- TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attgenerated)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
- errdetail("Column \"%s\" is a generated column.",
- NameStr(TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attname)),
- parser_errposition(pstate, var->location)));
- break;
- default:
- /* can't happen without add_missing_from, so just elog */
- elog(ERROR, "trigger WHEN condition cannot contain references to other relations");
- break;
- }
+ whenClause = stmt->whenClause;
+ Assert(whenClause != NULL);
}
+ else
+ whenClause = stmt->whenClause;
- /* we'll need the rtable for recordDependencyOnExpr */
- whenRtable = pstate->p_rtable;
-
- qual = nodeToString(whenClause);
-
- free_parsestate(pstate);
- }
- else if (!whenClause)
- {
- whenClause = NULL;
- whenRtable = NIL;
- qual = NULL;
- }
- else
- {
qual = nodeToString(whenClause);
- whenRtable = NIL;
}
/*
@@ -1129,10 +1016,40 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
* If it has a WHEN clause, add dependencies on objects mentioned in the
* expression (eg, functions, as well as any columns used).
*/
- if (whenRtable != NIL)
+ if (whenClause != NULL)
+ {
+ ParseState *pstate;
+ ParseNamespaceItem *nsitem;
+
+ /* Set up a pstate to parse with */
+ pstate = make_parsestate(NULL);
+ pstate->p_sourcetext = queryString;
+
+ /*
+ * Set up nsitems for OLD and NEW references.
+ *
+ * 'OLD' must always have varno equal to 1 and 'NEW' equal to 2.
+ */
+ nsitem = addRangeTableEntryForRelation(pstate, rel,
+ AccessShareLock,
+ makeAlias("old", NIL),
+ false, false);
+ addNSItemToQuery(pstate, nsitem, false, true, true);
+
+ nsitem = addRangeTableEntryForRelation(pstate, rel,
+ AccessShareLock,
+ makeAlias("new", NIL),
+ false, false);
+ addNSItemToQuery(pstate, nsitem, false, true, true);
+
+ /* we'll need the rtable for recordDependencyOnExpr */
+ whenRtable = pstate->p_rtable;
recordDependencyOnExpr(&myself, whenClause, whenRtable,
DEPENDENCY_NORMAL);
+ free_parsestate(pstate);
+ }
+
/* Post creation hook for new trigger */
InvokeObjectPostCreateHookArg(TriggerRelationId, trigoid, 0,
isInternal);
@@ -1175,7 +1092,6 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
*/
childStmt = copyObject(stmt);
childStmt->funcname = NIL;
- childStmt->whenClause = NULL;
/* If there is a WHEN clause, create a modified copy of it */
qual = copyObject(whenClause);
@@ -1185,11 +1101,13 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
qual = (Node *)
map_partition_varattnos((List *) qual, PRS2_NEW_VARNO,
childTbl, rel);
+ childStmt->whenClause = qual;
+ childStmt->transformed = true;
CreateTriggerFiringOn(childStmt, queryString,
partdesc->oids[i], refRelOid,
InvalidOid, InvalidOid,
- funcoid, trigoid, qual,
+ funcoid, trigoid,
isInternal, true, trigger_fires_when);
table_close(childTbl, NoLock);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9fd48acb1f8..05ff0c5973f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -6070,6 +6070,7 @@ CreateTrigStmt:
n->deferrable = false;
n->initdeferred = false;
n->constrrel = NULL;
+ n->transformed = false;
$$ = (Node *) n;
}
| CREATE opt_or_replace CONSTRAINT TRIGGER name AFTER TriggerEvents ON
@@ -6120,6 +6121,7 @@ CreateTrigStmt:
&n->deferrable, &n->initdeferred, &dummy,
NULL, NULL, yyscanner);
n->constrrel = $10;
+ n->transformed = false;
$$ = (Node *) n;
}
;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index e96b38a59d5..fd6d5c922ec 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -38,6 +38,7 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/comment.h"
#include "commands/defrem.h"
@@ -3133,6 +3134,149 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
return stmt;
}
+/*
+ * transformCreateTriggerStmt - parse analysis for CREATE TRIGGER
+ *
+ * Note: This is for parse analysis CreateTrigStmt->whenClause only, other
+ * CreateTrigStmt error checking happen in CreateTriggerFiringOn.
+ *
+ * To avoid race conditions, it's important that this function relies only on
+ * the passed-in relid (and not on stmt->relation) to determine the target
+ * relation.
+ */
+CreateTrigStmt *
+transformCreateTriggerStmt(Oid relid, CreateTrigStmt *stmt, const char *queryString)
+{
+ int16 tgtype;
+ ParseState *pstate;
+ ParseNamespaceItem *nsitem;
+ List *varList;
+ Relation rel;
+
+ /* Nothing to do if statement already transformed. */
+ if (stmt->transformed)
+ return stmt;
+
+ /* Compute tgtype */
+ TRIGGER_CLEAR_TYPE(tgtype);
+ if (stmt->row)
+ TRIGGER_SETT_ROW(tgtype);
+ tgtype |= stmt->timing;
+ tgtype |= stmt->events;
+
+ /* Set up a pstate to parse with */
+ pstate = make_parsestate(NULL);
+ pstate->p_sourcetext = queryString;
+
+ /*
+ * Put the parent table into the rtable so that the expressions can refer
+ * to its fields without qualification. Caller is responsible for locking
+ * relation, but we still need to open it.
+ */
+ rel = relation_open(relid, NoLock);
+
+ /*
+ * Set up nsitems for OLD and NEW references.
+ *
+ * 'OLD' must always have varno equal to 1 and 'NEW' equal to 2.
+ */
+ nsitem = addRangeTableEntryForRelation(pstate, rel,
+ AccessShareLock,
+ makeAlias("old", NIL),
+ false, false);
+ addNSItemToQuery(pstate, nsitem, false, true, true);
+ nsitem = addRangeTableEntryForRelation(pstate, rel,
+ AccessShareLock,
+ makeAlias("new", NIL),
+ false, false);
+ addNSItemToQuery(pstate, nsitem, false, true, true);
+
+ stmt->whenClause = transformWhereClause(pstate,
+ stmt->whenClause,
+ EXPR_KIND_TRIGGER_WHEN,
+ "WHEN");
+ /* we have to fix its collations too */
+ assign_expr_collations(pstate, stmt->whenClause);
+
+ /*
+ * Check for disallowed references to OLD/NEW.
+ *
+ * NB: pull_var_clause is okay here only because we don't allow
+ * subselects in WHEN clauses; it would fail to examine the contents
+ * of subselects.
+ */
+ varList = pull_var_clause(stmt->whenClause, 0);
+ foreach_node(Var, var, varList)
+ {
+ switch (var->varno)
+ {
+ case PRS2_OLD_VARNO:
+ if (!TRIGGER_FOR_ROW(tgtype))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("statement trigger's WHEN condition cannot reference column values"),
+ parser_errposition(pstate, var->location));
+ if (TRIGGER_FOR_INSERT(tgtype))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("INSERT trigger's WHEN condition cannot reference OLD values"),
+ parser_errposition(pstate, var->location));
+ /* system columns are okay here */
+ break;
+ case PRS2_NEW_VARNO:
+ if (!TRIGGER_FOR_ROW(tgtype))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("statement trigger's WHEN condition cannot reference column values"),
+ parser_errposition(pstate, var->location));
+ if (TRIGGER_FOR_DELETE(tgtype))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("DELETE trigger's WHEN condition cannot reference NEW values"),
+ parser_errposition(pstate, var->location));
+ if (var->varattno < 0 && TRIGGER_FOR_BEFORE(tgtype))
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("BEFORE trigger's WHEN condition cannot reference NEW system columns"),
+ parser_errposition(pstate, var->location));
+ if (TRIGGER_FOR_BEFORE(tgtype) &&
+ var->varattno == 0 &&
+ RelationGetDescr(rel)->constr &&
+ (RelationGetDescr(rel)->constr->has_generated_stored ||
+ RelationGetDescr(rel)->constr->has_generated_virtual))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
+ errdetail("A whole-row reference is used and the table contains generated columns."),
+ parser_errposition(pstate, var->location));
+ if (TRIGGER_FOR_BEFORE(tgtype) &&
+ var->varattno > 0 &&
+ TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attgenerated)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
+ errdetail("Column \"%s\" is a generated column.",
+ NameStr(TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attname)),
+ parser_errposition(pstate, var->location));
+ break;
+ default:
+ /* can't happen without add_missing_from, so just elog */
+ elog(ERROR, "trigger WHEN condition cannot contain references to other relations");
+ break;
+ }
+ }
+
+ free_parsestate(pstate);
+
+ /* Close relation */
+ table_close(rel, NoLock);
+
+ /* Mark statement as successfully transformed */
+ stmt->transformed = true;
+
+ return stmt;
+}
+
/*
* transformStatsStmt - parse analysis for CREATE STATISTICS
*
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 918db53dd5e..73d47b5ebf2 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1695,7 +1695,7 @@ ProcessUtilitySlow(ParseState *pstate,
address = CreateTrigger((CreateTrigStmt *) parsetree,
queryString, InvalidOid, InvalidOid,
InvalidOid, InvalidOid, InvalidOid,
- InvalidOid, NULL, false, false);
+ InvalidOid, false, false);
break;
case T_CreatePLangStmt:
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index cfd7daa20ed..3f4951bca61 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -153,12 +153,12 @@ extern PGDLLIMPORT int SessionReplicationRole;
extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
- Oid funcoid, Oid parentTriggerOid, Node *whenClause,
+ Oid funcoid, Oid parentTriggerOid,
bool isInternal, bool in_partition);
extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
Oid relOid, Oid refRelOid, Oid constraintOid,
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
- Node *whenClause, bool isInternal, bool in_partition,
+ bool isInternal, bool in_partition,
char trigger_fires_when);
extern void TriggerSetParentTrigger(Relation trigRel,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f1706df58fd..9d760e15145 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3122,6 +3122,7 @@ typedef struct CreateTrigStmt
bool deferrable; /* [NOT] DEFERRABLE */
bool initdeferred; /* INITIALLY {DEFERRED|IMMEDIATE} */
RangeVar *constrrel; /* opposite relation, if RI trigger */
+ bool transformed; /* true when transformCreateTriggerStmt is finished */
} CreateTrigStmt;
/* ----------------------
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 4965fac4495..5482719d997 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -28,6 +28,8 @@ extern IndexStmt *transformIndexStmt(Oid relid, IndexStmt *stmt,
const char *queryString);
extern CreateStatsStmt *transformStatsStmt(Oid relid, CreateStatsStmt *stmt,
const char *queryString);
+extern CreateTrigStmt *transformCreateTriggerStmt(Oid relid, CreateTrigStmt *stmt,
+ const char *queryString);
extern void transformRuleStmt(RuleStmt *stmt, const char *queryString,
List **actions, Node **whereClause);
extern List *transformCreateSchemaStmtElements(List *schemaElts,
--
2.34.1
On Mon, Sep 29, 2025 at 5:35 PM jian he <jian.universality@gmail.com> wrote:
hi.
poc demo:
CREATE TABLE main_table (a int, b int);
CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS '
BEGIN
RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'',
TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
RETURN NULL;
END;';
CREATE TRIGGER modified_a BEFORE UPDATE OF a ON main_table
FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE trigger_func('modified_a');CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS INCLUDING COMMENTS);
\d main_table1
Table "public.main_table1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | |
b | integer | | |
Triggers:
modified_a BEFORE UPDATE OF a ON main_table1 FOR EACH ROW WHEN
(old.a <> new.a) EXECUTE FUNCTION trigger_func('modified_a')foreign key associated internal triggers won't be copied to the new table.
source table trigger associated comment will be copied to the new table,
if INCLUDING COMMENTS is specified.
there are many table name as t1 in regress test, add tests like
""CREATE TABLE t1 (a int, b text, c int);"
may result in error
+ERROR: relation "t1" already exists
in some OS.
So I changed the table name to avoid parallel regess test failure.
Attachments:
v2-0001-refactor-CreateTrigger-and-CreateTriggerFiringOn.patchtext/x-patch; charset=US-ASCII; name=v2-0001-refactor-CreateTrigger-and-CreateTriggerFiringOn.patchDownload
From 396b47a1d6083f474db2e7abc51abef6093f3786 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Thu, 25 Sep 2025 16:41:36 +0800
Subject: [PATCH v2 1/2] refactor CreateTrigger and CreateTriggerFiringOn
discussion: https://postgr.es/m/
---
src/backend/catalog/index.c | 3 +-
src/backend/commands/tablecmds.c | 14 ++-
src/backend/commands/trigger.c | 180 ++++++++---------------------
src/backend/parser/gram.y | 2 +
src/backend/parser/parse_utilcmd.c | 144 +++++++++++++++++++++++
src/backend/tcop/utility.c | 2 +-
src/include/commands/trigger.h | 4 +-
src/include/nodes/parsenodes.h | 1 +
src/include/parser/parse_utilcmd.h | 2 +
9 files changed, 212 insertions(+), 140 deletions(-)
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 5d9db167e59..8d09d61cae2 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2039,10 +2039,11 @@ index_constraint_create(Relation heapRelation,
trigger->deferrable = true;
trigger->initdeferred = initdeferred;
trigger->constrrel = NULL;
+ trigger->transformed = true;
(void) CreateTrigger(trigger, NULL, RelationGetRelid(heapRelation),
InvalidOid, conOid, indexRelationId, InvalidOid,
- InvalidOid, NULL, true, false);
+ InvalidOid, true, false);
}
/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fc89352b661..bf8120c12f2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13802,10 +13802,11 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->deferrable = fkconstraint->deferrable;
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
+ fk_trigger->transformed = true;
trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
constraintOid, indexOid, InvalidOid,
- parentTrigOid, NULL, true, false);
+ parentTrigOid, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -13847,6 +13848,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
+ fk_trigger->transformed = true;
switch (fkconstraint->fk_del_action)
{
@@ -13883,7 +13885,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
- parentDelTrigger, NULL, true, false);
+ parentDelTrigger, true, false);
if (deleteTrigOid)
*deleteTrigOid = trigAddress.objectId;
@@ -13907,6 +13909,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
+ fk_trigger->transformed = true;
switch (fkconstraint->fk_upd_action)
{
@@ -13943,7 +13946,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
constraintOid, indexOid, InvalidOid,
- parentUpdTrigger, NULL, true, false);
+ parentUpdTrigger, true, false);
if (updateTrigOid)
*updateTrigOid = trigAddress.objectId;
}
@@ -20823,15 +20826,16 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK;
trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK;
trigStmt->columns = cols;
- trigStmt->whenClause = NULL; /* passed separately */
+ trigStmt->whenClause = qual;
trigStmt->transitionRels = NIL; /* not supported at present */
trigStmt->deferrable = trigForm->tgdeferrable;
trigStmt->initdeferred = trigForm->tginitdeferred;
trigStmt->constrrel = NULL; /* passed separately */
+ trigStmt->transformed = true; /* whenClause alerady transformed */
CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition),
trigForm->tgconstrrelid, InvalidOid, InvalidOid,
- trigForm->tgfoid, trigForm->oid, qual,
+ trigForm->tgfoid, trigForm->oid,
false, true, trigForm->tgenabled);
MemoryContextSwitchTo(oldcxt);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 579ac8d76ae..f1431d99a56 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -40,6 +40,7 @@
#include "parser/parse_collate.h"
#include "parser/parse_func.h"
#include "parser/parse_relation.h"
+#include "parser/parse_utilcmd.h"
#include "partitioning/partdesc.h"
#include "pgstat.h"
#include "rewrite/rewriteHandler.h"
@@ -139,9 +140,6 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
* create the trigger on partitions, 2) when creating child foreign key
* triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
- * If whenClause is passed, it is an already-transformed expression for
- * WHEN. In this case, we ignore any that may come in stmt->whenClause.
- *
* If isInternal is true then this is an internally-generated trigger.
* This argument sets the tgisinternal field of the pg_trigger entry, and
* if true causes us to modify the given trigger name to ensure uniqueness.
@@ -159,13 +157,13 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
ObjectAddress
CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
- Oid funcoid, Oid parentTriggerOid, Node *whenClause,
+ Oid funcoid, Oid parentTriggerOid,
bool isInternal, bool in_partition)
{
return
CreateTriggerFiringOn(stmt, queryString, relOid, refRelOid,
constraintOid, indexOid, funcoid,
- parentTriggerOid, whenClause, isInternal,
+ parentTriggerOid, isInternal,
in_partition, TRIGGER_FIRES_ON_ORIGIN);
}
@@ -177,15 +175,15 @@ ObjectAddress
CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
Oid relOid, Oid refRelOid, Oid constraintOid,
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
- Node *whenClause, bool isInternal, bool in_partition,
+ bool isInternal, bool in_partition,
char trigger_fires_when)
{
int16 tgtype;
int ncolumns;
int16 *columns;
int2vector *tgattr;
- List *whenRtable;
- char *qual;
+ List *whenRtable = NIL;
+ char *qual = NULL;
Datum values[Natts_pg_trigger];
bool nulls[Natts_pg_trigger];
Relation rel;
@@ -207,6 +205,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
bool existing_isClone = false;
+ Node *whenClause = NULL;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -557,133 +556,21 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
/*
* Parse the WHEN clause, if any and we weren't passed an already
* transformed one.
- *
- * Note that as a side effect, we fill whenRtable when parsing. If we got
- * an already parsed clause, this does not occur, which is what we want --
- * no point in adding redundant dependencies below.
*/
- if (!whenClause && stmt->whenClause)
+ if (stmt->whenClause)
{
- ParseState *pstate;
- ParseNamespaceItem *nsitem;
- List *varList;
- ListCell *lc;
-
- /* Set up a pstate to parse with */
- pstate = make_parsestate(NULL);
- pstate->p_sourcetext = queryString;
-
- /*
- * Set up nsitems for OLD and NEW references.
- *
- * 'OLD' must always have varno equal to 1 and 'NEW' equal to 2.
- */
- nsitem = addRangeTableEntryForRelation(pstate, rel,
- AccessShareLock,
- makeAlias("old", NIL),
- false, false);
- addNSItemToQuery(pstate, nsitem, false, true, true);
- nsitem = addRangeTableEntryForRelation(pstate, rel,
- AccessShareLock,
- makeAlias("new", NIL),
- false, false);
- addNSItemToQuery(pstate, nsitem, false, true, true);
-
- /* Transform expression. Copy to be sure we don't modify original */
- whenClause = transformWhereClause(pstate,
- copyObject(stmt->whenClause),
- EXPR_KIND_TRIGGER_WHEN,
- "WHEN");
- /* we have to fix its collations too */
- assign_expr_collations(pstate, whenClause);
-
- /*
- * Check for disallowed references to OLD/NEW.
- *
- * NB: pull_var_clause is okay here only because we don't allow
- * subselects in WHEN clauses; it would fail to examine the contents
- * of subselects.
- */
- varList = pull_var_clause(whenClause, 0);
- foreach(lc, varList)
+ if (!stmt->transformed)
{
- Var *var = (Var *) lfirst(lc);
+ stmt = transformCreateTriggerStmt(RelationGetRelid(rel), stmt,
+ queryString);
- switch (var->varno)
- {
- case PRS2_OLD_VARNO:
- if (!TRIGGER_FOR_ROW(tgtype))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("statement trigger's WHEN condition cannot reference column values"),
- parser_errposition(pstate, var->location)));
- if (TRIGGER_FOR_INSERT(tgtype))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("INSERT trigger's WHEN condition cannot reference OLD values"),
- parser_errposition(pstate, var->location)));
- /* system columns are okay here */
- break;
- case PRS2_NEW_VARNO:
- if (!TRIGGER_FOR_ROW(tgtype))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("statement trigger's WHEN condition cannot reference column values"),
- parser_errposition(pstate, var->location)));
- if (TRIGGER_FOR_DELETE(tgtype))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("DELETE trigger's WHEN condition cannot reference NEW values"),
- parser_errposition(pstate, var->location)));
- if (var->varattno < 0 && TRIGGER_FOR_BEFORE(tgtype))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("BEFORE trigger's WHEN condition cannot reference NEW system columns"),
- parser_errposition(pstate, var->location)));
- if (TRIGGER_FOR_BEFORE(tgtype) &&
- var->varattno == 0 &&
- RelationGetDescr(rel)->constr &&
- (RelationGetDescr(rel)->constr->has_generated_stored ||
- RelationGetDescr(rel)->constr->has_generated_virtual))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
- errdetail("A whole-row reference is used and the table contains generated columns."),
- parser_errposition(pstate, var->location)));
- if (TRIGGER_FOR_BEFORE(tgtype) &&
- var->varattno > 0 &&
- TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attgenerated)
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
- errdetail("Column \"%s\" is a generated column.",
- NameStr(TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attname)),
- parser_errposition(pstate, var->location)));
- break;
- default:
- /* can't happen without add_missing_from, so just elog */
- elog(ERROR, "trigger WHEN condition cannot contain references to other relations");
- break;
- }
+ whenClause = stmt->whenClause;
+ Assert(whenClause != NULL);
}
+ else
+ whenClause = stmt->whenClause;
- /* we'll need the rtable for recordDependencyOnExpr */
- whenRtable = pstate->p_rtable;
-
- qual = nodeToString(whenClause);
-
- free_parsestate(pstate);
- }
- else if (!whenClause)
- {
- whenClause = NULL;
- whenRtable = NIL;
- qual = NULL;
- }
- else
- {
qual = nodeToString(whenClause);
- whenRtable = NIL;
}
/*
@@ -1129,10 +1016,40 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
* If it has a WHEN clause, add dependencies on objects mentioned in the
* expression (eg, functions, as well as any columns used).
*/
- if (whenRtable != NIL)
+ if (whenClause != NULL)
+ {
+ ParseState *pstate;
+ ParseNamespaceItem *nsitem;
+
+ /* Set up a pstate to parse with */
+ pstate = make_parsestate(NULL);
+ pstate->p_sourcetext = queryString;
+
+ /*
+ * Set up nsitems for OLD and NEW references.
+ *
+ * 'OLD' must always have varno equal to 1 and 'NEW' equal to 2.
+ */
+ nsitem = addRangeTableEntryForRelation(pstate, rel,
+ AccessShareLock,
+ makeAlias("old", NIL),
+ false, false);
+ addNSItemToQuery(pstate, nsitem, false, true, true);
+
+ nsitem = addRangeTableEntryForRelation(pstate, rel,
+ AccessShareLock,
+ makeAlias("new", NIL),
+ false, false);
+ addNSItemToQuery(pstate, nsitem, false, true, true);
+
+ /* we'll need the rtable for recordDependencyOnExpr */
+ whenRtable = pstate->p_rtable;
recordDependencyOnExpr(&myself, whenClause, whenRtable,
DEPENDENCY_NORMAL);
+ free_parsestate(pstate);
+ }
+
/* Post creation hook for new trigger */
InvokeObjectPostCreateHookArg(TriggerRelationId, trigoid, 0,
isInternal);
@@ -1175,7 +1092,6 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
*/
childStmt = copyObject(stmt);
childStmt->funcname = NIL;
- childStmt->whenClause = NULL;
/* If there is a WHEN clause, create a modified copy of it */
qual = copyObject(whenClause);
@@ -1185,11 +1101,13 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
qual = (Node *)
map_partition_varattnos((List *) qual, PRS2_NEW_VARNO,
childTbl, rel);
+ childStmt->whenClause = qual;
+ childStmt->transformed = true;
CreateTriggerFiringOn(childStmt, queryString,
partdesc->oids[i], refRelOid,
InvalidOid, InvalidOid,
- funcoid, trigoid, qual,
+ funcoid, trigoid,
isInternal, true, trigger_fires_when);
table_close(childTbl, NoLock);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f1def67ac7c..1993f76f5c1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -6071,6 +6071,7 @@ CreateTrigStmt:
n->deferrable = false;
n->initdeferred = false;
n->constrrel = NULL;
+ n->transformed = false;
$$ = (Node *) n;
}
| CREATE opt_or_replace CONSTRAINT TRIGGER name AFTER TriggerEvents ON
@@ -6121,6 +6122,7 @@ CreateTrigStmt:
&n->deferrable, &n->initdeferred, &dummy,
NULL, NULL, yyscanner);
n->constrrel = $10;
+ n->transformed = false;
$$ = (Node *) n;
}
;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index e96b38a59d5..fd6d5c922ec 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -38,6 +38,7 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/comment.h"
#include "commands/defrem.h"
@@ -3133,6 +3134,149 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
return stmt;
}
+/*
+ * transformCreateTriggerStmt - parse analysis for CREATE TRIGGER
+ *
+ * Note: This is for parse analysis CreateTrigStmt->whenClause only, other
+ * CreateTrigStmt error checking happen in CreateTriggerFiringOn.
+ *
+ * To avoid race conditions, it's important that this function relies only on
+ * the passed-in relid (and not on stmt->relation) to determine the target
+ * relation.
+ */
+CreateTrigStmt *
+transformCreateTriggerStmt(Oid relid, CreateTrigStmt *stmt, const char *queryString)
+{
+ int16 tgtype;
+ ParseState *pstate;
+ ParseNamespaceItem *nsitem;
+ List *varList;
+ Relation rel;
+
+ /* Nothing to do if statement already transformed. */
+ if (stmt->transformed)
+ return stmt;
+
+ /* Compute tgtype */
+ TRIGGER_CLEAR_TYPE(tgtype);
+ if (stmt->row)
+ TRIGGER_SETT_ROW(tgtype);
+ tgtype |= stmt->timing;
+ tgtype |= stmt->events;
+
+ /* Set up a pstate to parse with */
+ pstate = make_parsestate(NULL);
+ pstate->p_sourcetext = queryString;
+
+ /*
+ * Put the parent table into the rtable so that the expressions can refer
+ * to its fields without qualification. Caller is responsible for locking
+ * relation, but we still need to open it.
+ */
+ rel = relation_open(relid, NoLock);
+
+ /*
+ * Set up nsitems for OLD and NEW references.
+ *
+ * 'OLD' must always have varno equal to 1 and 'NEW' equal to 2.
+ */
+ nsitem = addRangeTableEntryForRelation(pstate, rel,
+ AccessShareLock,
+ makeAlias("old", NIL),
+ false, false);
+ addNSItemToQuery(pstate, nsitem, false, true, true);
+ nsitem = addRangeTableEntryForRelation(pstate, rel,
+ AccessShareLock,
+ makeAlias("new", NIL),
+ false, false);
+ addNSItemToQuery(pstate, nsitem, false, true, true);
+
+ stmt->whenClause = transformWhereClause(pstate,
+ stmt->whenClause,
+ EXPR_KIND_TRIGGER_WHEN,
+ "WHEN");
+ /* we have to fix its collations too */
+ assign_expr_collations(pstate, stmt->whenClause);
+
+ /*
+ * Check for disallowed references to OLD/NEW.
+ *
+ * NB: pull_var_clause is okay here only because we don't allow
+ * subselects in WHEN clauses; it would fail to examine the contents
+ * of subselects.
+ */
+ varList = pull_var_clause(stmt->whenClause, 0);
+ foreach_node(Var, var, varList)
+ {
+ switch (var->varno)
+ {
+ case PRS2_OLD_VARNO:
+ if (!TRIGGER_FOR_ROW(tgtype))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("statement trigger's WHEN condition cannot reference column values"),
+ parser_errposition(pstate, var->location));
+ if (TRIGGER_FOR_INSERT(tgtype))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("INSERT trigger's WHEN condition cannot reference OLD values"),
+ parser_errposition(pstate, var->location));
+ /* system columns are okay here */
+ break;
+ case PRS2_NEW_VARNO:
+ if (!TRIGGER_FOR_ROW(tgtype))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("statement trigger's WHEN condition cannot reference column values"),
+ parser_errposition(pstate, var->location));
+ if (TRIGGER_FOR_DELETE(tgtype))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("DELETE trigger's WHEN condition cannot reference NEW values"),
+ parser_errposition(pstate, var->location));
+ if (var->varattno < 0 && TRIGGER_FOR_BEFORE(tgtype))
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("BEFORE trigger's WHEN condition cannot reference NEW system columns"),
+ parser_errposition(pstate, var->location));
+ if (TRIGGER_FOR_BEFORE(tgtype) &&
+ var->varattno == 0 &&
+ RelationGetDescr(rel)->constr &&
+ (RelationGetDescr(rel)->constr->has_generated_stored ||
+ RelationGetDescr(rel)->constr->has_generated_virtual))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
+ errdetail("A whole-row reference is used and the table contains generated columns."),
+ parser_errposition(pstate, var->location));
+ if (TRIGGER_FOR_BEFORE(tgtype) &&
+ var->varattno > 0 &&
+ TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attgenerated)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
+ errdetail("Column \"%s\" is a generated column.",
+ NameStr(TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attname)),
+ parser_errposition(pstate, var->location));
+ break;
+ default:
+ /* can't happen without add_missing_from, so just elog */
+ elog(ERROR, "trigger WHEN condition cannot contain references to other relations");
+ break;
+ }
+ }
+
+ free_parsestate(pstate);
+
+ /* Close relation */
+ table_close(rel, NoLock);
+
+ /* Mark statement as successfully transformed */
+ stmt->transformed = true;
+
+ return stmt;
+}
+
/*
* transformStatsStmt - parse analysis for CREATE STATISTICS
*
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 918db53dd5e..73d47b5ebf2 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1695,7 +1695,7 @@ ProcessUtilitySlow(ParseState *pstate,
address = CreateTrigger((CreateTrigStmt *) parsetree,
queryString, InvalidOid, InvalidOid,
InvalidOid, InvalidOid, InvalidOid,
- InvalidOid, NULL, false, false);
+ InvalidOid, false, false);
break;
case T_CreatePLangStmt:
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index cfd7daa20ed..3f4951bca61 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -153,12 +153,12 @@ extern PGDLLIMPORT int SessionReplicationRole;
extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
- Oid funcoid, Oid parentTriggerOid, Node *whenClause,
+ Oid funcoid, Oid parentTriggerOid,
bool isInternal, bool in_partition);
extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
Oid relOid, Oid refRelOid, Oid constraintOid,
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
- Node *whenClause, bool isInternal, bool in_partition,
+ bool isInternal, bool in_partition,
char trigger_fires_when);
extern void TriggerSetParentTrigger(Relation trigRel,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index ac0e02a1db7..7039ef25ab1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3124,6 +3124,7 @@ typedef struct CreateTrigStmt
bool deferrable; /* [NOT] DEFERRABLE */
bool initdeferred; /* INITIALLY {DEFERRED|IMMEDIATE} */
RangeVar *constrrel; /* opposite relation, if RI trigger */
+ bool transformed; /* true when transformCreateTriggerStmt is finished */
} CreateTrigStmt;
/* ----------------------
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 4965fac4495..5482719d997 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -28,6 +28,8 @@ extern IndexStmt *transformIndexStmt(Oid relid, IndexStmt *stmt,
const char *queryString);
extern CreateStatsStmt *transformStatsStmt(Oid relid, CreateStatsStmt *stmt,
const char *queryString);
+extern CreateTrigStmt *transformCreateTriggerStmt(Oid relid, CreateTrigStmt *stmt,
+ const char *queryString);
extern void transformRuleStmt(RuleStmt *stmt, const char *queryString,
List **actions, Node **whereClause);
extern List *transformCreateSchemaStmtElements(List *schemaElts,
--
2.34.1
v2-0002-CREATE-TABLE-LIKE-INCLUDING-TRIGGERS.patchtext/x-patch; charset=US-ASCII; name=v2-0002-CREATE-TABLE-LIKE-INCLUDING-TRIGGERS.patchDownload
From 873b7665bf687129cbf6798a555dcdafb5064f2e Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Wed, 1 Oct 2025 15:34:18 +0800
Subject: [PATCH v2 2/2] CREATE TABLE LIKE INCLUDING TRIGGERS
foreign key associated internal trigger won't being copied to new table. source
table trigger associated comment will copied to new table, if INCLUDING COMMENTS
is specified.
Currently not support CREATE FOREIGN TABLE LIKE. (maybe we should).
discussion: https://postgr.es/m/CACJufxHJAr2FjbeB6ghg_-N5dxX5JVnjKSLOUxOyt4TeaAWQkg@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/6087
---
doc/src/sgml/ref/create_table.sgml | 13 +-
src/backend/catalog/index.c | 1 +
src/backend/commands/tablecmds.c | 4 +
src/backend/commands/trigger.c | 6 +
src/backend/parser/gram.y | 7 +-
src/backend/parser/parse_utilcmd.c | 244 ++++++++++++++++++++++++-
src/include/nodes/parsenodes.h | 2 +
src/include/parser/kwlist.h | 1 +
src/test/regress/expected/triggers.out | 99 ++++++++++
src/test/regress/sql/triggers.sql | 43 +++++
10 files changed, 416 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index dc000e913c1..b1b33864ae7 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -88,7 +88,7 @@ class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable cl
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | TRIGGERS | ALL }
<phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
@@ -672,7 +672,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>INCLUDING COMMENTS</literal></term>
<listitem>
<para>
- Comments for the copied columns, constraints, and indexes will be
+ Comments for the copied columns, constraints, indexes and triggers will be
copied. The default behavior is to exclude comments, resulting in
the copied columns and constraints in the new table having no
comments.
@@ -776,6 +776,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-like-opt-triggers">
+ <term><literal>INCLUDING TRIGGERS</literal></term>
+ <listitem>
+ <para>
+ Any non-internal triggers on the original table will be created on the new table.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-parms-like-opt-all">
<term><literal>INCLUDING ALL</literal></term>
<listitem>
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 8d09d61cae2..5c0df73c1c1 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2040,6 +2040,7 @@ index_constraint_create(Relation heapRelation,
trigger->initdeferred = initdeferred;
trigger->constrrel = NULL;
trigger->transformed = true;
+ trigger->trigcomment = NULL;
(void) CreateTrigger(trigger, NULL, RelationGetRelid(heapRelation),
InvalidOid, conOid, indexRelationId, InvalidOid,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index bf8120c12f2..002b85c3d4b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13802,6 +13802,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->deferrable = fkconstraint->deferrable;
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
+ fk_trigger->trigcomment = NULL;
fk_trigger->transformed = true;
trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
@@ -13848,6 +13849,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
+ fk_trigger->trigcomment = NULL;
fk_trigger->transformed = true;
switch (fkconstraint->fk_del_action)
@@ -13909,6 +13911,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
+ fk_trigger->trigcomment = NULL;
fk_trigger->transformed = true;
switch (fkconstraint->fk_upd_action)
@@ -20831,6 +20834,7 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
trigStmt->deferrable = trigForm->tgdeferrable;
trigStmt->initdeferred = trigForm->tginitdeferred;
trigStmt->constrrel = NULL; /* passed separately */
+ trigStmt->trigcomment = NULL;
trigStmt->transformed = true; /* whenClause alerady transformed */
CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition),
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index f1431d99a56..324f31fc501 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -30,6 +30,7 @@
#include "catalog/pg_proc.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
+#include "commands/comment.h"
#include "commands/trigger.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -1122,6 +1123,11 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
/* Keep lock on target rel until end of xact */
table_close(rel, NoLock);
+ /* Add any requested comment */
+ if (stmt->trigcomment != NULL)
+ CreateComments(trigoid, TriggerRelationId, 0,
+ stmt->trigcomment);
+
return myself;
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 1993f76f5c1..2e650ed38f3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -777,7 +777,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
TABLE TABLES TABLESAMPLE TABLESPACE TARGET TEMP TEMPLATE TEMPORARY TEXT_P THEN
TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSFORM
- TREAT TRIGGER TRIM TRUE_P
+ TREAT TRIGGER TRIGGERS TRIM TRUE_P
TRUNCATE TRUSTED TYPE_P TYPES_P
UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN
@@ -4217,6 +4217,7 @@ TableLikeOption:
| INDEXES { $$ = CREATE_TABLE_LIKE_INDEXES; }
| STATISTICS { $$ = CREATE_TABLE_LIKE_STATISTICS; }
| STORAGE { $$ = CREATE_TABLE_LIKE_STORAGE; }
+ | TRIGGERS { $$ = CREATE_TABLE_LIKE_TRIGGERS; }
| ALL { $$ = CREATE_TABLE_LIKE_ALL; }
;
@@ -6071,6 +6072,7 @@ CreateTrigStmt:
n->deferrable = false;
n->initdeferred = false;
n->constrrel = NULL;
+ n->trigcomment = NULL;
n->transformed = false;
$$ = (Node *) n;
}
@@ -6122,6 +6124,7 @@ CreateTrigStmt:
&n->deferrable, &n->initdeferred, &dummy,
NULL, NULL, yyscanner);
n->constrrel = $10;
+ n->trigcomment = NULL;
n->transformed = false;
$$ = (Node *) n;
}
@@ -18042,6 +18045,7 @@ unreserved_keyword:
| TRANSACTION
| TRANSFORM
| TRIGGER
+ | TRIGGERS
| TRUNCATE
| TRUSTED
| TYPE_P
@@ -18691,6 +18695,7 @@ bare_label_keyword:
| TRANSFORM
| TREAT
| TRIGGER
+ | TRIGGERS
| TRIM
| TRUE_P
| TRUNCATE
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index fd6d5c922ec..7b8cccc4018 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -37,6 +37,7 @@
#include "catalog/pg_constraint.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
+#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
@@ -62,6 +63,7 @@
#include "rewrite/rewriteManip.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/partcache.h"
#include "utils/rel.h"
@@ -121,6 +123,11 @@ static CreateStatsStmt *generateClonedExtStatsStmt(RangeVar *heapRel,
Oid heapRelid,
Oid source_statsid,
const AttrMap *attmap);
+static CreateTrigStmt *generateClonedTriggerStmt(RangeVar *heapRel,
+ Oid source_trigid,
+ Relation source_rel,
+ const AttrMap *attmap);
+
static List *get_collation(Oid collation, Oid actual_datatype);
static List *get_opclass(Oid opclass, Oid actual_datatype);
static void transformIndexConstraints(CreateStmtContext *cxt);
@@ -1319,7 +1326,8 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
CREATE_TABLE_LIKE_GENERATED |
CREATE_TABLE_LIKE_CONSTRAINTS |
CREATE_TABLE_LIKE_INDEXES |
- CREATE_TABLE_LIKE_STATISTICS))
+ CREATE_TABLE_LIKE_STATISTICS |
+ CREATE_TABLE_LIKE_TRIGGERS))
{
table_like_clause->relationOid = RelationGetRelid(relation);
cxt->likeclauses = lappend(cxt->likeclauses, table_like_clause);
@@ -1585,6 +1593,46 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
}
}
+ /* Process triggers if required */
+ if ((table_like_clause->options & CREATE_TABLE_LIKE_TRIGGERS) &&
+ relation->trigdesc != NULL &&
+ childrel->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
+ {
+ bool include_comments;
+
+ include_comments = (table_like_clause->options & CREATE_TABLE_LIKE_COMMENTS);
+
+ for (int nt = 0; nt < relation->trigdesc->numtriggers; nt++)
+ {
+ Trigger *trig;
+ Oid trigoid = InvalidOid;
+ CreateTrigStmt *trig_stmt = NULL;
+
+ trig = relation->trigdesc->triggers + nt;
+ trigoid = trig->tgoid;
+
+ if (trig->tgisinternal)
+ continue;
+
+ /* internal trigger won't copied to new table */
+ trig_stmt = generateClonedTriggerStmt(heapRel,
+ trigoid,
+ relation,
+ attmap);
+
+ /* Copy comment on trigger, if requested */
+ if (include_comments)
+ {
+ comment = GetComment(trigoid, TriggerRelationId, 0);
+
+ /* We make use of CreateTrigStmt's trigcomment option */
+ trig_stmt->trigcomment = comment;
+ }
+
+ result = lappend(result, trig_stmt);
+ }
+ }
+
/*
* Process extended statistics if required.
*/
@@ -2164,6 +2212,200 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid,
return stats;
}
+/*
+ * Generate a CreateTrigStmt node using information from an already existing
+ * trigger "source_trigid" on source_rel, for the rel identified by heapRel.
+ * Ensure source trigger is not internal trigger.
+ *
+ * Attribute numbers in expression Vars are adjusted according to attmap.
+ */
+static CreateTrigStmt *
+generateClonedTriggerStmt(RangeVar *heapRel, Oid source_trigid,
+ Relation source_rel, const AttrMap *attmap)
+{
+ CreateTrigStmt *trigStmt;
+ HeapTuple triggerTuple;
+ HeapTuple proctup;
+ Form_pg_trigger trigForm;
+ Form_pg_proc procform;
+ Relation pg_trigger;
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Datum value;
+ bool isnull;
+ Node *qual = NULL;
+ List *trigargs = NIL;
+ List *cols = NIL;
+ List *funcname = NIL;
+ List *transitionRels = NIL;
+ char *schemaname;
+
+ pg_trigger = table_open(TriggerRelationId, AccessShareLock);
+
+ /* Find the trigger to copy */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(source_trigid));
+
+ tgscan = systable_beginscan(pg_trigger, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ triggerTuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(triggerTuple))
+ elog(ERROR, "could not find tuple for trigger %u", source_trigid);
+
+ trigForm = (Form_pg_trigger) GETSTRUCT(triggerTuple);
+
+ Assert(!trigForm->tgisinternal);
+
+ /* Reconstruct trigger function String list */
+ proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(trigForm->tgfoid));
+ if (!HeapTupleIsValid(proctup))
+ elog(ERROR, "cache lookup failed for function %u", trigForm->tgfoid);
+ procform = (Form_pg_proc) GETSTRUCT(proctup);
+
+ schemaname = get_namespace_name(procform->pronamespace);
+ funcname = list_make2(makeString(schemaname),
+ makeString(NameStr(procform->proname)));
+ ReleaseSysCache(proctup);
+
+ /* Reconstruct trigger arguments list */
+ if (trigForm->tgnargs > 0)
+ {
+ char *p;
+
+ value = heap_getattr(triggerTuple, Anum_pg_trigger_tgargs,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (isnull)
+ elog(ERROR, "tgargs is null for trigger \"%s\" in relation \"%s\"",
+ NameStr(trigForm->tgname),
+ RelationGetRelationName(source_rel));
+
+ p = (char *) VARDATA_ANY(DatumGetByteaPP(value));
+
+ for (int i = 0; i < trigForm->tgnargs; i++)
+ {
+ trigargs = lappend(trigargs, makeString(pstrdup(p)));
+ p += strlen(p) + 1;
+ }
+ }
+
+ /*
+ * If there is a column list, transform it to a list of column names.
+ * Note we don't need to map this list in any way ...
+ */
+ if (trigForm->tgattr.dim1 > 0)
+ {
+ int i;
+
+ for (i = 0; i < trigForm->tgattr.dim1; i++)
+ {
+ Form_pg_attribute col;
+
+ col = TupleDescAttr(RelationGetDescr(source_rel),
+ trigForm->tgattr.values[i] - 1);
+ cols = lappend(cols,
+ makeString(pstrdup(NameStr(col->attname))));
+ }
+ }
+
+ /* If the trigger has a WHEN qualification, add that */
+ value = fastgetattr(triggerTuple, Anum_pg_trigger_tgqual,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (!isnull)
+ {
+ bool found_whole_row;
+
+ qual = stringToNode(TextDatumGetCString(value));
+
+ /* Adjust Vars to match new table's column numbering */
+ qual = map_variable_attnos(qual, PRS2_NEW_VARNO, 0,
+ attmap,
+ InvalidOid, &found_whole_row);
+
+ /* As in expandTableLikeClause, reject whole-row variables */
+ if (found_whole_row)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot convert whole-row table reference"),
+ errdetail("Trigger \"%s\" contains a whole-row table reference.",
+ NameStr(trigForm->tgname)));
+
+ qual = map_variable_attnos(qual, PRS2_OLD_VARNO, 0,
+ attmap,
+ InvalidOid, &found_whole_row);
+
+ /* As in expandTableLikeClause, reject whole-row variables */
+ if (found_whole_row)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot convert whole-row table reference"),
+ errdetail("Trigger \"%s\" contains a whole-row table reference.",
+ NameStr(trigForm->tgname)));
+ }
+
+ /* Reconstruct trigger old transition table */
+ value = fastgetattr(triggerTuple, Anum_pg_trigger_tgoldtable,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (!isnull)
+ {
+ TriggerTransition *old = makeNode(TriggerTransition);
+ old->isNew = false;
+ old->name = NameStr(*DatumGetName(value));
+ old->isTable = true;
+ transitionRels = lappend(transitionRels, old);
+ }
+
+ /* Reconstruct trigger old transition table */
+ value = fastgetattr(triggerTuple, Anum_pg_trigger_tgnewtable,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (!isnull)
+ {
+ TriggerTransition *new = makeNode(TriggerTransition);
+ new->isNew = true;
+ new->name = NameStr(*DatumGetName(value));
+ new->isTable = true;
+ transitionRels = lappend(transitionRels, new);
+ }
+
+ trigStmt = makeNode(CreateTrigStmt);
+ trigStmt->replace = false;
+ trigStmt->isconstraint = OidIsValid(trigForm->tgconstraint);
+ trigStmt->trigname = NameStr(trigForm->tgname);
+ trigStmt->relation = heapRel;
+ trigStmt->funcname = funcname;
+ trigStmt->args = trigargs;
+ trigStmt->row = TRIGGER_FOR_ROW(trigForm->tgtype);
+ trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK;
+ trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK;
+ trigStmt->columns = cols;
+ trigStmt->whenClause = qual;
+ trigStmt->transitionRels = transitionRels;
+ trigStmt->deferrable = trigForm->tgdeferrable;
+ trigStmt->initdeferred = trigForm->tginitdeferred;
+
+ trigStmt->constrrel = NULL;
+ if (OidIsValid(trigForm->tgconstrrelid))
+ {
+ const char *relname;
+ const char *nspname;
+ Oid nspoid;
+
+ relname = quote_identifier(get_rel_name(trigForm->tgconstrrelid));
+ nspoid = get_rel_namespace(trigForm->tgconstrrelid);
+ nspname = get_namespace_name_or_temp(nspoid);
+ nspname = quote_identifier(nspname);
+ trigStmt->constrrel = makeRangeVar((char *) nspname, (char *) relname, -1);
+ }
+ trigStmt->transformed = true; /* don't need transformCreateTriggerStmt again */
+
+ systable_endscan(tgscan);
+ table_close(pg_trigger, AccessShareLock);
+
+ return trigStmt;
+}
+
/*
* get_collation - fetch qualified name of a collation
*
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7039ef25ab1..794a22168e7 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -794,6 +794,7 @@ typedef enum TableLikeOption
CREATE_TABLE_LIKE_INDEXES = 1 << 6,
CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+ CREATE_TABLE_LIKE_TRIGGERS = 1 << 9,
CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
} TableLikeOption;
@@ -3124,6 +3125,7 @@ typedef struct CreateTrigStmt
bool deferrable; /* [NOT] DEFERRABLE */
bool initdeferred; /* INITIALLY {DEFERRED|IMMEDIATE} */
RangeVar *constrrel; /* opposite relation, if RI trigger */
+ char *trigcomment; /* comment to apply to trigger, or NULL */
bool transformed; /* true when transformCreateTriggerStmt is finished */
} CreateTrigStmt;
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index a4af3f717a1..74cac2779f5 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -459,6 +459,7 @@ PG_KEYWORD("transaction", TRANSACTION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("transform", TRANSFORM, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("treat", TREAT, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("trigger", TRIGGER, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("triggers", TRIGGERS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("trim", TRIM, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("true", TRUE_P, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("truncate", TRUNCATE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 1eb8fba0953..f4c9807bf50 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -174,6 +174,16 @@ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_table
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt');
CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt');
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW WHEN (OLD is not null) EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+ERROR: cannot convert whole-row table reference
+DETAIL: Trigger "wholetrig" contains a whole-row table reference.
+DROP TRIGGER wholetrig ON main_table;
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW WHEN (NEW is not null) EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+ERROR: cannot convert whole-row table reference
+DETAIL: Trigger "wholetrig" contains a whole-row table reference.
+DROP TRIGGER wholetrig ON main_table;
--
-- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified,
-- CREATE TRIGGER should default to 'FOR EACH STATEMENT'
@@ -394,6 +404,50 @@ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER,
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+--create table like tests
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS 'trigger before_ins_stmt_trig';
+CREATE TABLE main_table1(c INT, LIKE main_table INCLUDING TRIGGERS INCLUDING COMMENTS);
+\d main_table
+ Table "public.main_table"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ after_ins_stmt_trig AFTER INSERT ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_ins_stmt')
+ after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_a_b_row')
+ after_upd_b_row_trig AFTER UPDATE OF b ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_b_row')
+ after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_b_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_ins_stmt')
+ before_upd_a_row_trig BEFORE UPDATE OF a ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('before_upd_a_row')
+ before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_upd_a_stmt')
+
+\d main_table1
+ Table "public.main_table1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ c | integer | | |
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ after_ins_stmt_trig AFTER INSERT ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_ins_stmt')
+ after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table1 FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_a_b_row')
+ after_upd_b_row_trig AFTER UPDATE OF b ON main_table1 FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_b_row')
+ after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_b_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_ins_stmt')
+ before_upd_a_row_trig BEFORE UPDATE OF a ON main_table1 FOR EACH ROW EXECUTE FUNCTION trigger_func('before_upd_a_row')
+ before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_upd_a_stmt')
+
+\dd before_ins_stmt_trig
+ Object descriptions
+ Schema | Name | Object | Description
+--------+----------------------+---------+------------------------------
+ public | before_ins_stmt_trig | trigger | trigger before_ins_stmt_trig
+(1 row)
+
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS NULL;
--
-- Test case for bug with BEFORE trigger followed by AFTER trigger with WHEN
--
@@ -861,6 +915,9 @@ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+CREATE TABLE main_view_table(LIKE main_view INCLUDING TRIGGERS); --error
+ERROR: "main_view_table" is a table
+DETAIL: Tables cannot have INSTEAD OF triggers.
-- Valid BEFORE statement VIEW triggers
CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
@@ -2318,6 +2375,18 @@ create constraint trigger parted_trig_two after insert on parted_constr
deferrable initially deferred enforced
for each row when (bark(new.b) AND new.a % 2 = 1)
execute procedure trigger_notice_ab();
+create table parted_constr_copy (like parted_constr including all);
+select pg_get_triggerdef(oid)
+from pg_trigger
+where not tgisinternal and tgrelid = 'parted_constr_copy'::regclass
+order by tgname;
+ pg_get_triggerdef
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE CONSTRAINT TRIGGER parted_trig AFTER INSERT ON public.parted_constr_copy DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE FUNCTION trigger_notice_ab()
+ CREATE CONSTRAINT TRIGGER parted_trig_two AFTER INSERT ON public.parted_constr_copy DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN ((bark(new.b) AND ((new.a % 2) = 1))) EXECUTE FUNCTION trigger_notice_ab()
+(2 rows)
+
+drop table parted_constr_copy;
-- The immediate constraint is fired immediately; the WHEN clause of the
-- deferred constraint is also called immediately. The deferred constraint
-- is fired at commit time.
@@ -3092,6 +3161,18 @@ create trigger iocdu_tt_parted_insert_trig
create trigger iocdu_tt_parted_update_trig
after update on iocdu_tt_parted referencing old table as old_table new table as new_table
for each statement execute procedure dump_update();
+CREATE TABLE iocdu_tt_parted_copy(LIKE iocdu_tt_parted INCLUDING TRIGGERS);
+\d iocdu_tt_parted_copy
+ Table "public.iocdu_tt_parted_copy"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | not null |
+ b | text | | |
+Triggers:
+ iocdu_tt_parted_insert_trig AFTER INSERT ON iocdu_tt_parted_copy REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION dump_insert()
+ iocdu_tt_parted_update_trig AFTER UPDATE ON iocdu_tt_parted_copy REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION dump_update()
+
+DROP TABLE iocdu_tt_parted_copy;
-- inserts only
insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB')
on conflict (a) do
@@ -3574,6 +3655,24 @@ begin
end;
$$;
alter function whoami() owner to regress_fn_owner;
+--CREATE TABLE LIKE INCLUDING TRIGGERS
+-- test constraint trigger that reference another table
+create table trig_t1 (id integer);
+create table trig_t2 (id integer);
+create constraint trigger con_trig_test after insert on trig_t1 from trig_t2
+ deferrable initially deferred
+ for each row
+ execute function whoami();
+create table trig_t1_copy(like trig_t1 including triggers);
+\d trig_t1_copy
+ Table "public.trig_t1_copy"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | |
+Triggers:
+ con_trig_test AFTER INSERT ON trig_t1_copy FROM trig_t2 DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION whoami()
+
+drop table trig_t1, trig_t2, trig_t1_copy;
create table defer_trig (id integer);
grant insert on defer_trig to public;
create constraint trigger whoami after insert on defer_trig
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 5f7f75d7ba5..cb9c6bfe38a 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -120,6 +120,14 @@ FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt');
CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt');
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW WHEN (OLD is not null) EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+DROP TRIGGER wholetrig ON main_table;
+
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW WHEN (NEW is not null) EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+DROP TRIGGER wholetrig ON main_table;
+
--
-- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified,
-- CREATE TRIGGER should default to 'FOR EACH STATEMENT'
@@ -234,6 +242,15 @@ SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = 'main_table'::regc
UPDATE main_table SET a = 50;
UPDATE main_table SET b = 10;
+--create table like tests
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS 'trigger before_ins_stmt_trig';
+CREATE TABLE main_table1(c INT, LIKE main_table INCLUDING TRIGGERS INCLUDING COMMENTS);
+\d main_table
+\d main_table1
+
+\dd before_ins_stmt_trig
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS NULL;
+
--
-- Test case for bug with BEFORE trigger followed by AFTER trigger with WHEN
--
@@ -621,6 +638,8 @@ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+CREATE TABLE main_view_table(LIKE main_view INCLUDING TRIGGERS); --error
+
-- Valid BEFORE statement VIEW triggers
CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
@@ -1608,6 +1627,14 @@ create constraint trigger parted_trig_two after insert on parted_constr
for each row when (bark(new.b) AND new.a % 2 = 1)
execute procedure trigger_notice_ab();
+create table parted_constr_copy (like parted_constr including all);
+select pg_get_triggerdef(oid)
+from pg_trigger
+where not tgisinternal and tgrelid = 'parted_constr_copy'::regclass
+order by tgname;
+
+drop table parted_constr_copy;
+
-- The immediate constraint is fired immediately; the WHEN clause of the
-- deferred constraint is also called immediately. The deferred constraint
-- is fired at commit time.
@@ -2278,6 +2305,10 @@ create trigger iocdu_tt_parted_update_trig
after update on iocdu_tt_parted referencing old table as old_table new table as new_table
for each statement execute procedure dump_update();
+CREATE TABLE iocdu_tt_parted_copy(LIKE iocdu_tt_parted INCLUDING TRIGGERS);
+\d iocdu_tt_parted_copy
+DROP TABLE iocdu_tt_parted_copy;
+
-- inserts only
insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB')
on conflict (a) do
@@ -2735,6 +2766,18 @@ end;
$$;
alter function whoami() owner to regress_fn_owner;
+--CREATE TABLE LIKE INCLUDING TRIGGERS
+-- test constraint trigger that reference another table
+create table trig_t1 (id integer);
+create table trig_t2 (id integer);
+create constraint trigger con_trig_test after insert on trig_t1 from trig_t2
+ deferrable initially deferred
+ for each row
+ execute function whoami();
+create table trig_t1_copy(like trig_t1 including triggers);
+\d trig_t1_copy
+drop table trig_t1, trig_t2, trig_t1_copy;
+
create table defer_trig (id integer);
grant insert on defer_trig to public;
create constraint trigger whoami after insert on defer_trig
--
2.34.1
hi.
in CreateTrigger, we have comments:
* relOid, if nonzero, is the relation on which the trigger should be
* created. If zero, the name provided in the statement will be looked up.
*
* refRelOid, if nonzero, is the relation to which the constraint trigger
* refers. If zero, the constraint relation name provided in the statement
* will be looked up as needed.
We can put these two parameters into the CreateTrigStmt.
change it from
CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition)
to:
CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition)
This is needed, ProcessUtilitySlow->CreateTrigger don't have the new
target table relation OID information, using CreateTrigStmt.relation
would cause repeated name lookup issue.
v3-0001 and v3-0002 refactor the CreateTrigger function.
The parameters relOid and refRelOid are removed and instead added to the
CreateTrigStmt structure.
These two patch (v3-0001, v3-0002) will also be used in [1]/messages/by-id/CACJufxGkqYrmwMdvUOUPet0443oUTgF_dKCpw3TfJiutfuywAQ@mail.gmail.com
[1]: /messages/by-id/CACJufxGkqYrmwMdvUOUPet0443oUTgF_dKCpw3TfJiutfuywAQ@mail.gmail.com
v3-0003 is for CREATE TABLE LIKE INCLUDING TRIGGERS.
Attachments:
v3-0002-add-constrrelOid-field-to-CreateTrigStmt.patchapplication/x-patch; name=v3-0002-add-constrrelOid-field-to-CreateTrigStmt.patchDownload
From 7859802d453d3161291dacef5468380f9d7780b0 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Sat, 27 Dec 2025 13:58:11 +0800
Subject: [PATCH v3 2/3] add constrrelOid field to CreateTrigStmt
In the spirit of the change made to add the relOid field to CreateTrigStmt.
discussion: https://postgr.es/m/CACJufxHJAr2FjbeB6ghg_-N5dxX5JVnjKSLOUxOyt4TeaAWQkg@mail.gmail.com
discussion: https://postgr.es/m/CACJufxGkqYrmwMdvUOUPet0443oUTgF_dKCpw3TfJiutfuywAQ@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/6087
commitfest: https://commitfest.postgresql.org/patch/6089
---
src/backend/catalog/index.c | 3 ++-
src/backend/commands/tablecmds.c | 13 ++++++++-----
src/backend/commands/trigger.c | 18 +++++++-----------
src/backend/parser/gram.y | 2 ++
src/backend/tcop/utility.c | 2 +-
src/include/commands/trigger.h | 4 ++--
src/include/nodes/parsenodes.h | 1 +
7 files changed, 23 insertions(+), 20 deletions(-)
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 8e74ff1dd33..b3b034cf3b8 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2040,9 +2040,10 @@ index_constraint_create(Relation heapRelation,
trigger->deferrable = true;
trigger->initdeferred = initdeferred;
trigger->constrrel = NULL;
+ trigger->constrrelOid = InvalidOid;
(void) CreateTrigger(trigger, NULL,
- InvalidOid, conOid, indexRelationId, InvalidOid,
+ conOid, indexRelationId, InvalidOid,
InvalidOid, NULL, true, false);
}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ee400c1cd58..cae3d5218aa 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13863,8 +13863,9 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->deferrable = fkconstraint->deferrable;
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
+ fk_trigger->constrrelOid = refRelOid;
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ trigAddress = CreateTrigger(fk_trigger, NULL,
constraintOid, indexOid, InvalidOid,
parentTrigOid, NULL, true, false);
@@ -13909,6 +13910,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
+ fk_trigger->constrrelOid = myRelOid;
switch (fkconstraint->fk_del_action)
{
@@ -13943,7 +13945,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid,
+ trigAddress = CreateTrigger(fk_trigger, NULL,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13970,6 +13972,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
+ fk_trigger->constrrelOid = myRelOid;
switch (fkconstraint->fk_upd_action)
{
@@ -14004,7 +14007,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid,
+ trigAddress = CreateTrigger(fk_trigger, NULL,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
@@ -20920,9 +20923,9 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
trigStmt->deferrable = trigForm->tgdeferrable;
trigStmt->initdeferred = trigForm->tginitdeferred;
trigStmt->constrrel = NULL; /* passed separately */
+ trigStmt->constrrelOid = trigForm->tgconstrrelid;
- CreateTriggerFiringOn(trigStmt, NULL,
- trigForm->tgconstrrelid, InvalidOid, InvalidOid,
+ CreateTriggerFiringOn(trigStmt, NULL, InvalidOid, InvalidOid,
trigForm->tgfoid, trigForm->oid, qual,
false, true, trigForm->tgenabled);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 6e079213863..18cd3cc41b3 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -111,10 +111,6 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
* queryString is the source text of the CREATE TRIGGER command.
* This must be supplied if a whenClause is specified, else it can be NULL.
*
- * refRelOid, if nonzero, is the relation to which the constraint trigger
- * refers. If zero, the constraint relation name provided in the statement
- * will be looked up as needed.
- *
* constraintOid, if nonzero, says that this trigger is being created
* internally to implement that constraint. A suitable pg_depend entry will
* be made to link the trigger to that constraint. constraintOid is zero when
@@ -155,12 +151,12 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
*/
ObjectAddress
CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
- Oid refRelOid, Oid constraintOid, Oid indexOid,
+ Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition)
{
return
- CreateTriggerFiringOn(stmt, queryString, refRelOid,
+ CreateTriggerFiringOn(stmt, queryString,
constraintOid, indexOid, funcoid,
parentTriggerOid, whenClause, isInternal,
in_partition, TRIGGER_FIRES_ON_ORIGIN);
@@ -172,7 +168,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
*/
ObjectAddress
CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
- Oid refRelOid, Oid constraintOid,
+ Oid constraintOid,
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when)
@@ -324,10 +320,10 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
* might end up creating a pg_constraint entry referencing a
* nonexistent table.
*/
- if (OidIsValid(refRelOid))
+ if (OidIsValid(stmt->constrrelOid))
{
- LockRelationOid(refRelOid, AccessShareLock);
- constrrelid = refRelOid;
+ LockRelationOid(stmt->constrrelOid, AccessShareLock);
+ constrrelid = stmt->constrrelOid;
}
else if (stmt->constrrel != NULL)
constrrelid = RangeVarGetRelid(stmt->constrrel, AccessShareLock,
@@ -1184,8 +1180,8 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
childTbl, rel);
childStmt->relOid = partdesc->oids[i];
+ childStmt->constrrelOid = stmt->constrrelOid;
CreateTriggerFiringOn(childStmt, queryString,
- refRelOid,
InvalidOid, InvalidOid,
funcoid, trigoid, qual,
isInternal, true, trigger_fires_when);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 01e66ba615e..4a0bab33e70 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -6151,6 +6151,7 @@ CreateTrigStmt:
n->deferrable = false;
n->initdeferred = false;
n->constrrel = NULL;
+ n->constrrelOid = InvalidOid;
$$ = (Node *) n;
}
| CREATE opt_or_replace CONSTRAINT TRIGGER name AFTER TriggerEvents ON
@@ -6202,6 +6203,7 @@ CreateTrigStmt:
&n->deferrable, &n->initdeferred, &dummy,
NULL, NULL, yyscanner);
n->constrrel = $10;
+ n->constrrelOid = InvalidOid;
$$ = (Node *) n;
}
;
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 7d6387ad9b5..6c3642e00bc 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1701,7 +1701,7 @@ ProcessUtilitySlow(ParseState *pstate,
case T_CreateTrigStmt:
address = CreateTrigger((CreateTrigStmt *) parsetree,
- queryString, InvalidOid,
+ queryString,
InvalidOid, InvalidOid, InvalidOid,
InvalidOid, NULL, false, false);
break;
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index ae7abd456bf..a2b69576093 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -152,11 +152,11 @@ extern PGDLLIMPORT int SessionReplicationRole;
#define TRIGGER_DISABLED 'D'
extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
- Oid refRelOid, Oid constraintOid, Oid indexOid,
+ Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition);
extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
- Oid refRelOid, Oid constraintOid,
+ Oid constraintOid,
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f876f00ab9f..a3bf11e4fb7 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3164,6 +3164,7 @@ typedef struct CreateTrigStmt
bool deferrable; /* [NOT] DEFERRABLE */
bool initdeferred; /* INITIALLY {DEFERRED|IMMEDIATE} */
RangeVar *constrrel; /* opposite relation, if RI trigger */
+ Oid constrrelOid; /* opposite relation Oid, if RI trigger */
} CreateTrigStmt;
/* ----------------------
--
2.34.1
v3-0001-add-relOid-field-to-CreateTrigStmt.patchapplication/x-patch; name=v3-0001-add-relOid-field-to-CreateTrigStmt.patchDownload
From c18ea0dbc66832a5eba9ce9cf959583dfc2d594c Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Sat, 27 Dec 2025 13:55:59 +0800
Subject: [PATCH v3 1/3] add relOid field to CreateTrigStmt
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
add the relOid field to CreateTrigStmt is very useful for copying or recreating
a CreateTrigStmt.
Copying CreateTrigStmt node can happen like CREATE TABLE LIKE INCLUDING TRIGGERS.
To let ALTER COLUMN SET DATA TYPE cope with trigger dependencies, we need get
the trigger definition then do parse analysis for CreateTrigStmt again.
in function CreateTrigger, we have below comments:
```
* relOid, if nonzero, is the relation on which the trigger should be * created.
If zero, the name provided in the statement will be looked up.
```
We can move the "relOid" argument out of the CreateTrigger function’s parameter
list and instead store it in CreateTrigStmt.
The following are the reason why:
1. CreateTrigger has too many arguments; reducing the argument list by one is a good thing.
2. To implement CREATE TABLE LIKE INCLUDING TRIGGERS, we need to pass the new
relation OID to CreateTrigger, but it's not doable from
ProcessUtilitySlow->CreateTrigger.
3. To allow ALTER COLUMN SET DATA TYPE to cope with trigger dependencies, we
also need to pass the new relation OID to CreateTrigger, which for the same
reason as above cannot be done from ProcessUtilitySlow. This can help to reduce
repeated lookup issue.
discussion: https://postgr.es/m/CACJufxHJAr2FjbeB6ghg_-N5dxX5JVnjKSLOUxOyt4TeaAWQkg@mail.gmail.com
discussion: https://postgr.es/m/CACJufxGkqYrmwMdvUOUPet0443oUTgF_dKCpw3TfJiutfuywAQ@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/6087
commitfest: https://commitfest.postgresql.org/patch/6089
---
src/backend/catalog/index.c | 3 ++-
src/backend/commands/tablecmds.c | 12 ++++++++----
src/backend/commands/trigger.c | 16 +++++++---------
src/backend/parser/gram.y | 2 ++
src/backend/tcop/utility.c | 2 +-
src/include/commands/trigger.h | 4 ++--
src/include/nodes/parsenodes.h | 11 +++++++++++
7 files changed, 33 insertions(+), 17 deletions(-)
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 8dea58ad96b..8e74ff1dd33 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2028,6 +2028,7 @@ index_constraint_create(Relation heapRelation,
"PK_ConstraintTrigger" :
"Unique_ConstraintTrigger";
trigger->relation = NULL;
+ trigger->relOid = RelationGetRelid(heapRelation);
trigger->funcname = SystemFuncName("unique_key_recheck");
trigger->args = NIL;
trigger->row = true;
@@ -2040,7 +2041,7 @@ index_constraint_create(Relation heapRelation,
trigger->initdeferred = initdeferred;
trigger->constrrel = NULL;
- (void) CreateTrigger(trigger, NULL, RelationGetRelid(heapRelation),
+ (void) CreateTrigger(trigger, NULL,
InvalidOid, conOid, indexRelationId, InvalidOid,
InvalidOid, NULL, true, false);
}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1d9565b09fc..ee400c1cd58 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13840,6 +13840,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->isconstraint = true;
fk_trigger->trigname = "RI_ConstraintTrigger_c";
fk_trigger->relation = NULL;
+ fk_trigger->relOid = myRelOid;
/* Either ON INSERT or ON UPDATE */
if (on_insert)
@@ -13863,7 +13864,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
constraintOid, indexOid, InvalidOid,
parentTrigOid, NULL, true, false);
@@ -13899,6 +13900,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->isconstraint = true;
fk_trigger->trigname = "RI_ConstraintTrigger_a";
fk_trigger->relation = NULL;
+ fk_trigger->relOid = refRelOid;
fk_trigger->args = NIL;
fk_trigger->row = true;
fk_trigger->timing = TRIGGER_TYPE_AFTER;
@@ -13941,7 +13943,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13959,6 +13961,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->isconstraint = true;
fk_trigger->trigname = "RI_ConstraintTrigger_a";
fk_trigger->relation = NULL;
+ fk_trigger->relOid = refRelOid;
fk_trigger->args = NIL;
fk_trigger->row = true;
fk_trigger->timing = TRIGGER_TYPE_AFTER;
@@ -14001,7 +14004,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
@@ -20905,6 +20908,7 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
trigStmt->isconstraint = OidIsValid(trigForm->tgconstraint);
trigStmt->trigname = NameStr(trigForm->tgname);
trigStmt->relation = NULL;
+ trigStmt->relOid = RelationGetRelid(partition);
trigStmt->funcname = NULL; /* passed separately */
trigStmt->args = trigargs;
trigStmt->row = true;
@@ -20917,7 +20921,7 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
trigStmt->initdeferred = trigForm->tginitdeferred;
trigStmt->constrrel = NULL; /* passed separately */
- CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition),
+ CreateTriggerFiringOn(trigStmt, NULL,
trigForm->tgconstrrelid, InvalidOid, InvalidOid,
trigForm->tgfoid, trigForm->oid, qual,
false, true, trigForm->tgenabled);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 12c97f2c023..6e079213863 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -111,9 +111,6 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
* queryString is the source text of the CREATE TRIGGER command.
* This must be supplied if a whenClause is specified, else it can be NULL.
*
- * relOid, if nonzero, is the relation on which the trigger should be
- * created. If zero, the name provided in the statement will be looked up.
- *
* refRelOid, if nonzero, is the relation to which the constraint trigger
* refers. If zero, the constraint relation name provided in the statement
* will be looked up as needed.
@@ -158,12 +155,12 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
*/
ObjectAddress
CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
- Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
+ Oid refRelOid, Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition)
{
return
- CreateTriggerFiringOn(stmt, queryString, relOid, refRelOid,
+ CreateTriggerFiringOn(stmt, queryString, refRelOid,
constraintOid, indexOid, funcoid,
parentTriggerOid, whenClause, isInternal,
in_partition, TRIGGER_FIRES_ON_ORIGIN);
@@ -175,7 +172,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
*/
ObjectAddress
CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
- Oid relOid, Oid refRelOid, Oid constraintOid,
+ Oid refRelOid, Oid constraintOid,
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when)
@@ -208,8 +205,8 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
bool existing_isInternal = false;
bool existing_isClone = false;
- if (OidIsValid(relOid))
- rel = table_open(relOid, ShareRowExclusiveLock);
+ if (OidIsValid(stmt->relOid))
+ rel = table_open(stmt->relOid, ShareRowExclusiveLock);
else
rel = table_openrv(stmt->relation, ShareRowExclusiveLock);
@@ -1186,8 +1183,9 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
map_partition_varattnos((List *) qual, PRS2_NEW_VARNO,
childTbl, rel);
+ childStmt->relOid = partdesc->oids[i];
CreateTriggerFiringOn(childStmt, queryString,
- partdesc->oids[i], refRelOid,
+ refRelOid,
InvalidOid, InvalidOid,
funcoid, trigoid, qual,
isInternal, true, trigger_fires_when);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 28f4e11e30f..01e66ba615e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -6139,6 +6139,7 @@ CreateTrigStmt:
n->isconstraint = false;
n->trigname = $4;
n->relation = $8;
+ n->relOid = InvalidOid;
n->funcname = $14;
n->args = $16;
n->row = $10;
@@ -6188,6 +6189,7 @@ CreateTrigStmt:
n->isconstraint = true;
n->trigname = $5;
n->relation = $9;
+ n->relOid = InvalidOid;
n->funcname = $18;
n->args = $20;
n->row = true;
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index d18a3a60a46..7d6387ad9b5 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1701,7 +1701,7 @@ ProcessUtilitySlow(ParseState *pstate,
case T_CreateTrigStmt:
address = CreateTrigger((CreateTrigStmt *) parsetree,
- queryString, InvalidOid, InvalidOid,
+ queryString, InvalidOid,
InvalidOid, InvalidOid, InvalidOid,
InvalidOid, NULL, false, false);
break;
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index cfd7daa20ed..ae7abd456bf 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -152,11 +152,11 @@ extern PGDLLIMPORT int SessionReplicationRole;
#define TRIGGER_DISABLED 'D'
extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
- Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
+ Oid refRelOid, Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition);
extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
- Oid relOid, Oid refRelOid, Oid constraintOid,
+ Oid refRelOid, Oid constraintOid,
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index bc7adba4a0f..f876f00ab9f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3138,6 +3138,17 @@ typedef struct CreateTrigStmt
bool isconstraint; /* This is a constraint trigger */
char *trigname; /* TRIGGER's name */
RangeVar *relation; /* relation trigger is on */
+
+ /*
+ * The OID of the relation on which the trigger is to be created. If this
+ * is InvalidOid, CreateTrigStmt.relation is used to perform the lookup;
+ * otherwise, use this directly. This is useful when CreateTrigger is
+ * invoked indirectly rather than directly from the parser.
+ *
+ * Using the OID also avoids repeated relation name lookups.
+ */
+ Oid relOid;
+
List *funcname; /* qual. name of function to call */
List *args; /* list of String or NIL */
bool row; /* ROW/STATEMENT */
--
2.34.1
v3-0003-CREATE-TABLE-LIKE-INCLUDING-TRIGGERS.patchapplication/x-patch; name=v3-0003-CREATE-TABLE-LIKE-INCLUDING-TRIGGERS.patchDownload
From d4f6cfec79a7b1b18b357d7326752d72efd5e2d7 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 29 Dec 2025 08:58:09 +0800
Subject: [PATCH v3 3/3] CREATE TABLE LIKE INCLUDING TRIGGERS
This will copy all source table's trigger to the new table. Internal trigger
(such as foreign key associated trigger) won't being copied to new table.
However this command will fail if the source table's trigger contain whole-row
reference.
Trigger's comment will copied to new table, if INCLUDING COMMENTS is specified.
CREATE FOREIGN TABLE LIKE is also supported.
discussion: https://postgr.es/m/CACJufxHJAr2FjbeB6ghg_-N5dxX5JVnjKSLOUxOyt4TeaAWQkg@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/6087
---
doc/src/sgml/ref/create_foreign_table.sgml | 11 +-
doc/src/sgml/ref/create_table.sgml | 13 +-
src/backend/catalog/index.c | 2 +
src/backend/commands/tablecmds.c | 8 +
src/backend/commands/trigger.c | 212 +++++++++++++++++-
src/backend/parser/gram.y | 9 +-
src/backend/parser/parse_utilcmd.c | 43 +++-
src/include/commands/trigger.h | 3 +-
src/include/nodes/parsenodes.h | 4 +
src/include/parser/kwlist.h | 1 +
.../regress/expected/create_table_like.out | 6 +
src/test/regress/expected/triggers.out | 123 ++++++++++
src/test/regress/sql/create_table_like.sql | 4 +
src/test/regress/sql/triggers.sql | 51 +++++
14 files changed, 477 insertions(+), 13 deletions(-)
diff --git a/doc/src/sgml/ref/create_foreign_table.sgml b/doc/src/sgml/ref/create_foreign_table.sgml
index 08a8ceeae75..68d0608b0a3 100644
--- a/doc/src/sgml/ref/create_foreign_table.sgml
+++ b/doc/src/sgml/ref/create_foreign_table.sgml
@@ -60,7 +60,7 @@ CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
-{ INCLUDING | EXCLUDING } { COMMENTS | CONSTRAINTS | DEFAULTS | GENERATED | STATISTICS | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | CONSTRAINTS | DEFAULTS | GENERATED | STATISTICS | TRIGGERS | ALL }
<phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
@@ -284,6 +284,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>INCLUDING TRIGGERS</literal></term>
+ <listitem>
+ <para>
+ All non-internal triggers are copied to the new table.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>INCLUDING ALL</literal></term>
<listitem>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 77c5a763d45..c6a0f6dae9b 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -88,7 +88,7 @@ class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable cl
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | TRIGGERS | ALL }
<phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
@@ -672,7 +672,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>INCLUDING COMMENTS</literal></term>
<listitem>
<para>
- Comments for the copied columns, constraints, and indexes will be
+ Comments for the copied columns, constraints, indexes and triggers will be
copied. The default behavior is to exclude comments, resulting in
the copied columns and constraints in the new table having no
comments.
@@ -776,6 +776,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-like-opt-triggers">
+ <term><literal>INCLUDING TRIGGERS</literal></term>
+ <listitem>
+ <para>
+ All non-internal triggers on the original table will be created on the new table.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-parms-like-opt-all">
<term><literal>INCLUDING ALL</literal></term>
<listitem>
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index b3b034cf3b8..e6e21302713 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2041,6 +2041,8 @@ index_constraint_create(Relation heapRelation,
trigger->initdeferred = initdeferred;
trigger->constrrel = NULL;
trigger->constrrelOid = InvalidOid;
+ trigger->trigcomment = NULL;
+ trigger->transformed = true;
(void) CreateTrigger(trigger, NULL,
conOid, indexRelationId, InvalidOid,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cae3d5218aa..78ef3935d0a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13864,6 +13864,8 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
fk_trigger->constrrelOid = refRelOid;
+ fk_trigger->trigcomment = NULL;
+ fk_trigger->transformed = true;
trigAddress = CreateTrigger(fk_trigger, NULL,
constraintOid, indexOid, InvalidOid,
@@ -13911,6 +13913,8 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
fk_trigger->constrrelOid = myRelOid;
+ fk_trigger->trigcomment = NULL;
+ fk_trigger->transformed = true;
switch (fkconstraint->fk_del_action)
{
@@ -13973,6 +13977,8 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
fk_trigger->constrrelOid = myRelOid;
+ fk_trigger->trigcomment = NULL;
+ fk_trigger->transformed = true;
switch (fkconstraint->fk_upd_action)
{
@@ -20924,6 +20930,8 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
trigStmt->initdeferred = trigForm->tginitdeferred;
trigStmt->constrrel = NULL; /* passed separately */
trigStmt->constrrelOid = trigForm->tgconstrrelid;
+ trigStmt->trigcomment = NULL;
+ trigStmt->transformed = true;
CreateTriggerFiringOn(trigStmt, NULL, InvalidOid, InvalidOid,
trigForm->tgfoid, trigForm->oid, qual,
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 18cd3cc41b3..ef5ea69076f 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -30,6 +30,7 @@
#include "catalog/pg_proc.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
+#include "commands/comment.h"
#include "commands/trigger.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -582,13 +583,20 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
false, false);
addNSItemToQuery(pstate, nsitem, false, true, true);
- /* Transform expression. Copy to be sure we don't modify original */
- whenClause = transformWhereClause(pstate,
- copyObject(stmt->whenClause),
- EXPR_KIND_TRIGGER_WHEN,
- "WHEN");
- /* we have to fix its collations too */
- assign_expr_collations(pstate, whenClause);
+ if (stmt->transformed)
+ whenClause = stmt->whenClause;
+ else
+ {
+ /* Transform expression. Copy to be sure we don't modify original */
+ whenClause = transformWhereClause(pstate,
+ copyObject(stmt->whenClause),
+ EXPR_KIND_TRIGGER_WHEN,
+ "WHEN");
+ /* we have to fix its collations too */
+ assign_expr_collations(pstate, whenClause);
+
+ stmt->transformed = true;
+ }
/*
* Check for disallowed references to OLD/NEW.
@@ -1198,6 +1206,11 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
/* Keep lock on target rel until end of xact */
table_close(rel, NoLock);
+ /* Add any requested comment */
+ if (stmt->trigcomment != NULL)
+ CreateComments(trigoid, TriggerRelationId, 0,
+ stmt->trigcomment);
+
return myself;
}
@@ -6709,3 +6722,188 @@ check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple tuple)
return tuple;
}
+
+/*
+ * Copy trigger definition to the new relation: Populate CreateTrigStmt using
+ * the existing trigger (source_trigid on source_rel) for the target relation
+ * (heapRel).
+ *
+ * Attribute numbers in expression Vars are adjusted according to attmap.
+ */
+void
+generateClonedTriggerStmt(CreateTrigStmt *trigStmt, RangeVar *heapRel, Oid source_trigid,
+ Relation source_rel, const AttrMap *attmap)
+{
+ HeapTuple triggerTuple;
+ HeapTuple proctup;
+ Form_pg_trigger trigForm;
+ Form_pg_proc procform;
+ Relation pg_trigger;
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Datum value;
+ bool isnull;
+ Node *qual = NULL;
+ List *trigargs = NIL;
+ List *cols = NIL;
+ List *funcname = NIL;
+ List *transitionRels = NIL;
+ char *schemaname;
+
+ pg_trigger = table_open(TriggerRelationId, AccessShareLock);
+
+ /* Find the trigger to copy */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(source_trigid));
+
+ tgscan = systable_beginscan(pg_trigger, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ triggerTuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(triggerTuple))
+ elog(ERROR, "could not find tuple for trigger %u", source_trigid);
+
+ trigForm = (Form_pg_trigger) GETSTRUCT(triggerTuple);
+
+ Assert(!trigForm->tgisinternal);
+
+ /* Reconstruct trigger function String list */
+ proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(trigForm->tgfoid));
+ if (!HeapTupleIsValid(proctup))
+ elog(ERROR, "cache lookup failed for function %u", trigForm->tgfoid);
+ procform = (Form_pg_proc) GETSTRUCT(proctup);
+
+ schemaname = get_namespace_name(procform->pronamespace);
+ funcname = list_make2(makeString(schemaname),
+ makeString(NameStr(procform->proname)));
+ ReleaseSysCache(proctup);
+
+ /* Reconstruct trigger arguments list */
+ if (trigForm->tgnargs > 0)
+ {
+ bytea *val;
+ char *p;
+
+ val = DatumGetByteaPP(fastgetattr(triggerTuple,
+ Anum_pg_trigger_tgargs,
+ RelationGetDescr(pg_trigger),
+ &isnull));
+ if (isnull)
+ elog(ERROR, "tgargs is null in trigger \"%s\" for relation \"%s\"",
+ NameStr(trigForm->tgname),
+ RelationGetRelationName(source_rel));
+
+ p = (char *) VARDATA_ANY(val);
+
+ for (int i = 0; i < trigForm->tgnargs; i++)
+ {
+ trigargs = lappend(trigargs, makeString(pstrdup(p)));
+ p += strlen(p) + 1;
+ }
+ }
+
+ /*
+ * If there is a column list, transform it to a list of column names. Note
+ * we don't need to map this list in any way ...
+ */
+ if (trigForm->tgattr.dim1 > 0)
+ {
+ for (int i = 0; i < trigForm->tgattr.dim1; i++)
+ {
+ Form_pg_attribute col;
+
+ col = TupleDescAttr(RelationGetDescr(source_rel),
+ trigForm->tgattr.values[i] - 1);
+ cols = lappend(cols,
+ makeString(pstrdup(NameStr(col->attname))));
+ }
+ }
+
+ /* If the trigger has a WHEN qualification, add that */
+ value = fastgetattr(triggerTuple, Anum_pg_trigger_tgqual,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (!isnull)
+ {
+ bool found_whole_row;
+
+ qual = stringToNode(TextDatumGetCString(value));
+
+ /* Adjust Vars to match new table's column numbering */
+ qual = map_variable_attnos(qual, PRS2_NEW_VARNO, 0,
+ attmap,
+ InvalidOid,
+ &found_whole_row);
+
+ /* As in expandTableLikeClause, reject whole-row variables */
+ if (found_whole_row)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot convert whole-row table reference"),
+ errdetail("Trigger \"%s\" contains a whole-row table reference.",
+ NameStr(trigForm->tgname)));
+
+ qual = map_variable_attnos(qual, PRS2_OLD_VARNO, 0,
+ attmap,
+ InvalidOid,
+ &found_whole_row);
+
+ /* As in expandTableLikeClause, reject whole-row variables */
+ if (found_whole_row)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot convert whole-row table reference"),
+ errdetail("Trigger \"%s\" contains a whole-row table reference.",
+ NameStr(trigForm->tgname)));
+ }
+
+ /* Reconstruct trigger old transition table */
+ value = fastgetattr(triggerTuple, Anum_pg_trigger_tgoldtable,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (!isnull)
+ {
+ TriggerTransition *old = makeNode(TriggerTransition);
+
+ old->isNew = false;
+ old->name = NameStr(*DatumGetName(value));
+ old->isTable = true;
+ transitionRels = lappend(transitionRels, old);
+ }
+
+ /* Reconstruct trigger old transition table */
+ value = fastgetattr(triggerTuple, Anum_pg_trigger_tgnewtable,
+ RelationGetDescr(pg_trigger),
+ &isnull);
+ if (!isnull)
+ {
+ TriggerTransition *new = makeNode(TriggerTransition);
+
+ new->isNew = true;
+ new->name = NameStr(*DatumGetName(value));
+ new->isTable = true;
+ transitionRels = lappend(transitionRels, new);
+ }
+
+ trigStmt->replace = false;
+ trigStmt->isconstraint = OidIsValid(trigForm->tgconstraint);
+ trigStmt->trigname = NameStr(trigForm->tgname);
+ trigStmt->relation = heapRel;
+ Assert(OidIsValid(trigStmt->relOid));
+ trigStmt->funcname = funcname;
+ trigStmt->args = trigargs;
+ trigStmt->row = TRIGGER_FOR_ROW(trigForm->tgtype);
+ trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK;
+ trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK;
+ trigStmt->columns = cols;
+ trigStmt->whenClause = qual;
+ trigStmt->transitionRels = transitionRels;
+ trigStmt->deferrable = trigForm->tgdeferrable;
+ trigStmt->initdeferred = trigForm->tginitdeferred;
+ trigStmt->constrrelOid = trigForm->tgconstrrelid;
+ trigStmt->trigcomment = NULL;
+ trigStmt->transformed = true;
+
+ systable_endscan(tgscan);
+ table_close(pg_trigger, AccessShareLock);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4a0bab33e70..50fc189613e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -786,7 +786,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
TABLE TABLES TABLESAMPLE TABLESPACE TARGET TEMP TEMPLATE TEMPORARY TEXT_P THEN
TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSFORM
- TREAT TRIGGER TRIM TRUE_P
+ TREAT TRIGGER TRIGGERS TRIM TRUE_P
TRUNCATE TRUSTED TYPE_P TYPES_P
UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN
@@ -4296,6 +4296,7 @@ TableLikeOption:
| INDEXES { $$ = CREATE_TABLE_LIKE_INDEXES; }
| STATISTICS { $$ = CREATE_TABLE_LIKE_STATISTICS; }
| STORAGE { $$ = CREATE_TABLE_LIKE_STORAGE; }
+ | TRIGGERS { $$ = CREATE_TABLE_LIKE_TRIGGERS; }
| ALL { $$ = CREATE_TABLE_LIKE_ALL; }
;
@@ -6152,6 +6153,8 @@ CreateTrigStmt:
n->initdeferred = false;
n->constrrel = NULL;
n->constrrelOid = InvalidOid;
+ n->trigcomment = NULL;
+ n->transformed = false;
$$ = (Node *) n;
}
| CREATE opt_or_replace CONSTRAINT TRIGGER name AFTER TriggerEvents ON
@@ -6204,6 +6207,8 @@ CreateTrigStmt:
NULL, NULL, yyscanner);
n->constrrel = $10;
n->constrrelOid = InvalidOid;
+ n->trigcomment = NULL;
+ n->transformed = false;
$$ = (Node *) n;
}
;
@@ -18193,6 +18198,7 @@ unreserved_keyword:
| TRANSACTION
| TRANSFORM
| TRIGGER
+ | TRIGGERS
| TRUNCATE
| TRUSTED
| TYPE_P
@@ -18846,6 +18852,7 @@ bare_label_keyword:
| TRANSFORM
| TREAT
| TRIGGER
+ | TRIGGERS
| TRIM
| TRUE_P
| TRUNCATE
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 2b7b084f216..8d36e0b4ae2 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -39,12 +39,14 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/comment.h"
#include "commands/defrem.h"
#include "commands/sequence.h"
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
+#include "commands/trigger.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
@@ -1321,7 +1323,8 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
CREATE_TABLE_LIKE_GENERATED |
CREATE_TABLE_LIKE_CONSTRAINTS |
CREATE_TABLE_LIKE_INDEXES |
- CREATE_TABLE_LIKE_STATISTICS))
+ CREATE_TABLE_LIKE_STATISTICS |
+ CREATE_TABLE_LIKE_TRIGGERS))
{
table_like_clause->relationOid = RelationGetRelid(relation);
cxt->likeclauses = lappend(cxt->likeclauses, table_like_clause);
@@ -1587,6 +1590,44 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
}
}
+ /* Process triggers if required */
+ if ((table_like_clause->options & CREATE_TABLE_LIKE_TRIGGERS) &&
+ relation->trigdesc != NULL)
+ {
+ bool include_comments;
+ CreateTrigStmt *trig_stmt = NULL;
+
+ include_comments = (table_like_clause->options & CREATE_TABLE_LIKE_COMMENTS);
+
+ for (int nt = 0; nt < relation->trigdesc->numtriggers; nt++)
+ {
+ Trigger *trig = relation->trigdesc->triggers + nt;
+ Oid trigoid = trig->tgoid;
+
+ /* internal trigger won't copied to new table */
+ if (trig->tgisinternal)
+ continue;
+
+ trig_stmt = makeNode(CreateTrigStmt);
+
+ trig_stmt->relOid = RelationGetRelid(childrel);
+
+ generateClonedTriggerStmt(trig_stmt, heapRel, trigoid, relation,
+ attmap);
+
+ /* Copy comment on trigger, if requested */
+ if (include_comments)
+ {
+ comment = GetComment(trigoid, TriggerRelationId, 0);
+
+ /* We make use of CreateTrigStmt's trigcomment option */
+ trig_stmt->trigcomment = comment;
+ }
+
+ result = lappend(result, trig_stmt);
+ }
+ }
+
/*
* Process extended statistics if required.
*/
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index a2b69576093..e8494ad9e0a 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -266,7 +266,8 @@ extern void AfterTriggerBeginSubXact(void);
extern void AfterTriggerEndSubXact(bool isCommit);
extern void AfterTriggerSetState(ConstraintsSetStmt *stmt);
extern bool AfterTriggerPendingOnRel(Oid relid);
-
+extern void generateClonedTriggerStmt(CreateTrigStmt *trigStmt, RangeVar *heapRel, Oid source_trigid,
+ Relation source_rel, const AttrMap *attmap);
/*
* in utils/adt/ri_triggers.c
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index a3bf11e4fb7..ded02f81ba1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -795,6 +795,7 @@ typedef enum TableLikeOption
CREATE_TABLE_LIKE_INDEXES = 1 << 6,
CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+ CREATE_TABLE_LIKE_TRIGGERS = 1 << 9,
CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
} TableLikeOption;
@@ -3165,6 +3166,9 @@ typedef struct CreateTrigStmt
bool initdeferred; /* INITIALLY {DEFERRED|IMMEDIATE} */
RangeVar *constrrel; /* opposite relation, if RI trigger */
Oid constrrelOid; /* opposite relation Oid, if RI trigger */
+ char *trigcomment; /* comment to apply to trigger, or NULL */
+ bool transformed; /* true means no need do parse analysis for
+ * whenClause */
} CreateTrigStmt;
/* ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 9fde58f541c..479a157332c 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -464,6 +464,7 @@ PG_KEYWORD("transaction", TRANSACTION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("transform", TRANSFORM, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("treat", TREAT, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("trigger", TRIGGER, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("triggers", TRIGGERS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("trim", TRIM, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("true", TRUE_P, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("truncate", TRUNCATE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index d3c35c14847..8451bccbd13 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -631,6 +631,10 @@ Not-null constraints:
"ctl_table_a_not_null" NOT NULL "a"
"ctl_table_d_not_null" NOT NULL "d"
+-- trigger_func was created in triggers.sql
+CREATE TRIGGER trigtest_before_stmt BEFORE DELETE OR UPDATE ON ctl_table
+FOR EACH ROW WHEN (OLD.a > 0)
+EXECUTE PROCEDURE trigger_func('trigtest_before_stmt');
-- Test EXCLUDING ALL
CREATE FOREIGN TABLE ctl_foreign_table1(LIKE ctl_table EXCLUDING ALL) SERVER ctl_s0;
\d+ ctl_foreign_table1
@@ -679,6 +683,8 @@ Statistics objects:
Not-null constraints:
"ctl_table_a_not_null" NOT NULL "a"
"ctl_table_d_not_null" NOT NULL "d"
+Triggers:
+ trigtest_before_stmt BEFORE DELETE OR UPDATE ON ctl_foreign_table2 FOR EACH ROW WHEN (old.a > 0) EXECUTE FUNCTION trigger_func('trigtest_before_stmt')
Server: ctl_s0
-- \d+ does not report the value of attcompression for a foreign table, so
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 1eb8fba0953..089c051eec3 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -165,6 +165,7 @@ drop table trigtest;
CREATE TABLE log_table (tstamp timestamp default timeofday()::timestamp);
CREATE TABLE main_table (a int unique, b int);
COPY main_table (a,b) FROM stdin;
+-- This function also used in create_table_like.sql, don't drop it.
CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS '
BEGIN
RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
@@ -174,6 +175,20 @@ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_table
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt');
CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt');
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW
+WHEN (OLD is not null)
+EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+ERROR: cannot convert whole-row table reference
+DETAIL: Trigger "wholetrig" contains a whole-row table reference.
+DROP TRIGGER wholetrig ON main_table;
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW
+WHEN (NEW is not null)
+EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+ERROR: cannot convert whole-row table reference
+DETAIL: Trigger "wholetrig" contains a whole-row table reference.
+DROP TRIGGER wholetrig ON main_table;
--
-- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified,
-- CREATE TRIGGER should default to 'FOR EACH STATEMENT'
@@ -394,6 +409,53 @@ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER,
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+--create table like tests
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS 'trigger before_ins_stmt_trig';
+CREATE TABLE main_table1(c INT, LIKE main_table INCLUDING TRIGGERS INCLUDING COMMENTS);
+\d main_table
+ Table "public.main_table"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ after_ins_stmt_trig AFTER INSERT ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_ins_stmt')
+ after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_a_b_row')
+ after_upd_b_row_trig AFTER UPDATE OF b ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_b_row')
+ after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_b_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_ins_stmt')
+ before_upd_a_row_trig BEFORE UPDATE OF a ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('before_upd_a_row')
+ before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_upd_a_stmt')
+
+\d main_table1
+ Table "public.main_table1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ c | integer | | |
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ after_ins_stmt_trig AFTER INSERT ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_ins_stmt')
+ after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table1 FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_a_b_row')
+ after_upd_b_row_trig AFTER UPDATE OF b ON main_table1 FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_b_row')
+ after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_b_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_ins_stmt')
+ before_upd_a_row_trig BEFORE UPDATE OF a ON main_table1 FOR EACH ROW EXECUTE FUNCTION trigger_func('before_upd_a_row')
+ before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_upd_a_stmt')
+
+SELECT pc.relname, pd.tgname, obj_description(pd.oid, 'pg_trigger')
+ FROM pg_trigger pd JOIN pg_class pc ON (pc.oid = pd.tgrelid)
+ AND pd.tgname = 'before_ins_stmt_trig'
+ ORDER BY 1;
+ relname | tgname | obj_description
+-------------+----------------------+------------------------------
+ main_table | before_ins_stmt_trig | trigger before_ins_stmt_trig
+ main_table1 | before_ins_stmt_trig | trigger before_ins_stmt_trig
+(2 rows)
+
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS NULL;
--
-- Test case for bug with BEFORE trigger followed by AFTER trigger with WHEN
--
@@ -422,6 +484,20 @@ NOTICE: dummy_update_func(afterb) called: action = UPDATE, old = (t), new = (f)
UPDATE some_t SET some_col = TRUE;
NOTICE: dummy_update_func(before) called: action = UPDATE, old = (f), new = (t)
NOTICE: dummy_update_func(aftera) called: action = UPDATE, old = (f), new = (t)
+CREATE TABLE some_t1 (c INT, LIKE some_t INCLUDING TRIGGERS INCLUDING COMMENTS);
+\d+ some_t1
+ Table "public.some_t1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+----------+---------+-----------+----------+---------+---------+--------------+-------------
+ c | integer | | | | plain | |
+ some_col | boolean | | not null | | plain | |
+Not-null constraints:
+ "some_t_some_col_not_null" NOT NULL "some_col"
+Triggers:
+ some_trig_aftera AFTER UPDATE ON some_t1 FOR EACH ROW WHEN (NOT old.some_col AND new.some_col) EXECUTE FUNCTION dummy_update_func('aftera')
+ some_trig_afterb AFTER UPDATE ON some_t1 FOR EACH ROW WHEN (NOT new.some_col) EXECUTE FUNCTION dummy_update_func('afterb')
+ some_trig_before BEFORE UPDATE ON some_t1 FOR EACH ROW EXECUTE FUNCTION dummy_update_func('before')
+
DROP TABLE some_t;
-- bogus cases
CREATE TRIGGER error_upd_and_col BEFORE UPDATE OR UPDATE OF a ON main_table
@@ -861,6 +937,9 @@ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+CREATE TABLE main_view_table(LIKE main_view INCLUDING TRIGGERS); --error
+ERROR: "main_view_table" is a table
+DETAIL: Tables cannot have INSTEAD OF triggers.
-- Valid BEFORE statement VIEW triggers
CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
@@ -2318,6 +2397,20 @@ create constraint trigger parted_trig_two after insert on parted_constr
deferrable initially deferred enforced
for each row when (bark(new.b) AND new.a % 2 = 1)
execute procedure trigger_notice_ab();
+create table parted_constr_copy (like parted_constr including all);
+select pg_get_triggerdef(oid)
+from pg_trigger
+where not tgisinternal and tgrelid in ('parted_constr_copy'::regclass, 'parted_constr'::regclass)
+order by tgname;
+ pg_get_triggerdef
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE CONSTRAINT TRIGGER parted_trig AFTER INSERT ON public.parted_constr DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE FUNCTION trigger_notice_ab()
+ CREATE CONSTRAINT TRIGGER parted_trig AFTER INSERT ON public.parted_constr_copy DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE FUNCTION trigger_notice_ab()
+ CREATE CONSTRAINT TRIGGER parted_trig_two AFTER INSERT ON public.parted_constr DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN ((bark(new.b) AND ((new.a % 2) = 1))) EXECUTE FUNCTION trigger_notice_ab()
+ CREATE CONSTRAINT TRIGGER parted_trig_two AFTER INSERT ON public.parted_constr_copy DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN ((bark(new.b) AND ((new.a % 2) = 1))) EXECUTE FUNCTION trigger_notice_ab()
+(4 rows)
+
+drop table parted_constr_copy;
-- The immediate constraint is fired immediately; the WHEN clause of the
-- deferred constraint is also called immediately. The deferred constraint
-- is fired at commit time.
@@ -3092,6 +3185,18 @@ create trigger iocdu_tt_parted_insert_trig
create trigger iocdu_tt_parted_update_trig
after update on iocdu_tt_parted referencing old table as old_table new table as new_table
for each statement execute procedure dump_update();
+CREATE TABLE iocdu_tt_parted_copy(LIKE iocdu_tt_parted INCLUDING TRIGGERS);
+\d iocdu_tt_parted_copy
+ Table "public.iocdu_tt_parted_copy"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | not null |
+ b | text | | |
+Triggers:
+ iocdu_tt_parted_insert_trig AFTER INSERT ON iocdu_tt_parted_copy REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION dump_insert()
+ iocdu_tt_parted_update_trig AFTER UPDATE ON iocdu_tt_parted_copy REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION dump_update()
+
+DROP TABLE iocdu_tt_parted_copy;
-- inserts only
insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB')
on conflict (a) do
@@ -3574,6 +3679,24 @@ begin
end;
$$;
alter function whoami() owner to regress_fn_owner;
+--CREATE TABLE LIKE INCLUDING TRIGGERS
+-- test constraint trigger that reference another table
+create table trig_t1 (id integer);
+create table trig_t2 (id integer);
+create constraint trigger con_trig_test after insert on trig_t1 from trig_t2
+ deferrable initially deferred
+ for each row
+ execute function whoami();
+create table trig_t1_copy(like trig_t1 including triggers);
+\d trig_t1_copy
+ Table "public.trig_t1_copy"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | |
+Triggers:
+ con_trig_test AFTER INSERT ON trig_t1_copy FROM trig_t2 DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION whoami()
+
+drop table trig_t1, trig_t2, trig_t1_copy;
create table defer_trig (id integer);
grant insert on defer_trig to public;
create constraint trigger whoami after insert on defer_trig
diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql
index 93389b57dbf..34ceeb76d92 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -258,6 +258,10 @@ ALTER TABLE ctl_table ADD CONSTRAINT foo CHECK (b = 'text');
ALTER TABLE ctl_table ALTER COLUMN b SET STORAGE MAIN;
\d+ ctl_table
+-- trigger_func was created in triggers.sql
+CREATE TRIGGER trigtest_before_stmt BEFORE DELETE OR UPDATE ON ctl_table
+FOR EACH ROW WHEN (OLD.a > 0)
+EXECUTE PROCEDURE trigger_func('trigtest_before_stmt');
-- Test EXCLUDING ALL
CREATE FOREIGN TABLE ctl_foreign_table1(LIKE ctl_table EXCLUDING ALL) SERVER ctl_s0;
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 5f7f75d7ba5..bf56d18c870 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -108,6 +108,7 @@ COPY main_table (a,b) FROM stdin;
80 15
\.
+-- This function also used in create_table_like.sql, don't drop it.
CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS '
BEGIN
RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
@@ -120,6 +121,18 @@ FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt');
CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt');
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW
+WHEN (OLD is not null)
+EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+DROP TRIGGER wholetrig ON main_table;
+
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW
+WHEN (NEW is not null)
+EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+DROP TRIGGER wholetrig ON main_table;
+
--
-- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified,
-- CREATE TRIGGER should default to 'FOR EACH STATEMENT'
@@ -234,6 +247,17 @@ SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = 'main_table'::regc
UPDATE main_table SET a = 50;
UPDATE main_table SET b = 10;
+--create table like tests
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS 'trigger before_ins_stmt_trig';
+CREATE TABLE main_table1(c INT, LIKE main_table INCLUDING TRIGGERS INCLUDING COMMENTS);
+\d main_table
+\d main_table1
+SELECT pc.relname, pd.tgname, obj_description(pd.oid, 'pg_trigger')
+ FROM pg_trigger pd JOIN pg_class pc ON (pc.oid = pd.tgrelid)
+ AND pd.tgname = 'before_ins_stmt_trig'
+ ORDER BY 1;
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS NULL;
+
--
-- Test case for bug with BEFORE trigger followed by AFTER trigger with WHEN
--
@@ -258,6 +282,8 @@ INSERT INTO some_t VALUES (TRUE);
UPDATE some_t SET some_col = TRUE;
UPDATE some_t SET some_col = FALSE;
UPDATE some_t SET some_col = TRUE;
+CREATE TABLE some_t1 (c INT, LIKE some_t INCLUDING TRIGGERS INCLUDING COMMENTS);
+\d+ some_t1
DROP TABLE some_t;
-- bogus cases
@@ -621,6 +647,8 @@ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+CREATE TABLE main_view_table(LIKE main_view INCLUDING TRIGGERS); --error
+
-- Valid BEFORE statement VIEW triggers
CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
@@ -1608,6 +1636,13 @@ create constraint trigger parted_trig_two after insert on parted_constr
for each row when (bark(new.b) AND new.a % 2 = 1)
execute procedure trigger_notice_ab();
+create table parted_constr_copy (like parted_constr including all);
+select pg_get_triggerdef(oid)
+from pg_trigger
+where not tgisinternal and tgrelid in ('parted_constr_copy'::regclass, 'parted_constr'::regclass)
+order by tgname;
+drop table parted_constr_copy;
+
-- The immediate constraint is fired immediately; the WHEN clause of the
-- deferred constraint is also called immediately. The deferred constraint
-- is fired at commit time.
@@ -2278,6 +2313,10 @@ create trigger iocdu_tt_parted_update_trig
after update on iocdu_tt_parted referencing old table as old_table new table as new_table
for each statement execute procedure dump_update();
+CREATE TABLE iocdu_tt_parted_copy(LIKE iocdu_tt_parted INCLUDING TRIGGERS);
+\d iocdu_tt_parted_copy
+DROP TABLE iocdu_tt_parted_copy;
+
-- inserts only
insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB')
on conflict (a) do
@@ -2735,6 +2774,18 @@ end;
$$;
alter function whoami() owner to regress_fn_owner;
+--CREATE TABLE LIKE INCLUDING TRIGGERS
+-- test constraint trigger that reference another table
+create table trig_t1 (id integer);
+create table trig_t2 (id integer);
+create constraint trigger con_trig_test after insert on trig_t1 from trig_t2
+ deferrable initially deferred
+ for each row
+ execute function whoami();
+create table trig_t1_copy(like trig_t1 including triggers);
+\d trig_t1_copy
+drop table trig_t1, trig_t2, trig_t1_copy;
+
create table defer_trig (id integer);
grant insert on defer_trig to public;
create constraint trigger whoami after insert on defer_trig
--
2.34.1
On Mon, Dec 29, 2025 at 9:26 AM jian he <jian.universality@gmail.com> wrote:
v3-0001 and v3-0002 refactor the CreateTrigger function.
The parameters relOid and refRelOid are removed and instead added to the
CreateTrigStmt structure.These two patch (v3-0001, v3-0002) will also be used in [1]
[1]: /messages/by-id/CACJufxGkqYrmwMdvUOUPet0443oUTgF_dKCpw3TfJiutfuywAQ@mail.gmail.comv3-0003 is for CREATE TABLE LIKE INCLUDING TRIGGERS.
hi.
https://cirrus-ci.com/build/6583555523870720
https://api.cirrus-ci.com/v1/artifact/task/5664491007901696/testrun/build/testrun/pg_upgrade/002_pg_upgrade/data/regression.diffs
only FreeBSD fails. I suspect this is because pstrdup was not used in
generateClonedTriggerStmt.
now using pstrdup in generateClonedTriggerStmt; otherwise v4-0003 is identical
to v3-0003.
Attachments:
v4-0001-add-relOid-field-to-CreateTrigStmt.patchtext/x-patch; charset=UTF-8; name=v4-0001-add-relOid-field-to-CreateTrigStmt.patchDownload
From 0815620d5611183d3da1163d2d4a9330b280d7dd Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Sat, 27 Dec 2025 13:55:59 +0800
Subject: [PATCH v4 1/3] add relOid field to CreateTrigStmt
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
add the relOid field to CreateTrigStmt is very useful for copying or recreating
a CreateTrigStmt.
Copying CreateTrigStmt node can happen like CREATE TABLE LIKE INCLUDING TRIGGERS.
To let ALTER COLUMN SET DATA TYPE cope with trigger dependencies, we need get
the trigger definition then do parse analysis for CreateTrigStmt again.
in function CreateTrigger, we have below comments:
```
* relOid, if nonzero, is the relation on which the trigger should be * created.
If zero, the name provided in the statement will be looked up.
```
We can move the "relOid" argument out of the CreateTrigger function’s parameter
list and instead store it in CreateTrigStmt.
The following are the reason why:
1. CreateTrigger has too many arguments; reducing the argument list by one is a good thing.
2. To implement CREATE TABLE LIKE INCLUDING TRIGGERS, we need to pass the new
relation OID to CreateTrigger, but it's not doable from
ProcessUtilitySlow->CreateTrigger.
3. To allow ALTER COLUMN SET DATA TYPE to cope with trigger dependencies, we
also need to pass the new relation OID to CreateTrigger, which for the same
reason as above cannot be done from ProcessUtilitySlow. This can help to reduce
repeated lookup issue.
discussion: https://postgr.es/m/CACJufxHJAr2FjbeB6ghg_-N5dxX5JVnjKSLOUxOyt4TeaAWQkg@mail.gmail.com
discussion: https://postgr.es/m/CACJufxGkqYrmwMdvUOUPet0443oUTgF_dKCpw3TfJiutfuywAQ@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/6087
commitfest: https://commitfest.postgresql.org/patch/6089
---
src/backend/catalog/index.c | 3 ++-
src/backend/commands/tablecmds.c | 12 ++++++++----
src/backend/commands/trigger.c | 16 +++++++---------
src/backend/parser/gram.y | 2 ++
src/backend/tcop/utility.c | 2 +-
src/include/commands/trigger.h | 4 ++--
src/include/nodes/parsenodes.h | 11 +++++++++++
7 files changed, 33 insertions(+), 17 deletions(-)
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 86db0541f29..4afcb2872e7 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2026,6 +2026,7 @@ index_constraint_create(Relation heapRelation,
"PK_ConstraintTrigger" :
"Unique_ConstraintTrigger";
trigger->relation = NULL;
+ trigger->relOid = RelationGetRelid(heapRelation);
trigger->funcname = SystemFuncName("unique_key_recheck");
trigger->args = NIL;
trigger->row = true;
@@ -2038,7 +2039,7 @@ index_constraint_create(Relation heapRelation,
trigger->initdeferred = initdeferred;
trigger->constrrel = NULL;
- (void) CreateTrigger(trigger, NULL, RelationGetRelid(heapRelation),
+ (void) CreateTrigger(trigger, NULL,
InvalidOid, conOid, indexRelationId, InvalidOid,
InvalidOid, NULL, true, false);
}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1d9565b09fc..ee400c1cd58 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13840,6 +13840,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->isconstraint = true;
fk_trigger->trigname = "RI_ConstraintTrigger_c";
fk_trigger->relation = NULL;
+ fk_trigger->relOid = myRelOid;
/* Either ON INSERT or ON UPDATE */
if (on_insert)
@@ -13863,7 +13864,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
constraintOid, indexOid, InvalidOid,
parentTrigOid, NULL, true, false);
@@ -13899,6 +13900,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->isconstraint = true;
fk_trigger->trigname = "RI_ConstraintTrigger_a";
fk_trigger->relation = NULL;
+ fk_trigger->relOid = refRelOid;
fk_trigger->args = NIL;
fk_trigger->row = true;
fk_trigger->timing = TRIGGER_TYPE_AFTER;
@@ -13941,7 +13943,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13959,6 +13961,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->isconstraint = true;
fk_trigger->trigname = "RI_ConstraintTrigger_a";
fk_trigger->relation = NULL;
+ fk_trigger->relOid = refRelOid;
fk_trigger->args = NIL;
fk_trigger->row = true;
fk_trigger->timing = TRIGGER_TYPE_AFTER;
@@ -14001,7 +14004,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
@@ -20905,6 +20908,7 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
trigStmt->isconstraint = OidIsValid(trigForm->tgconstraint);
trigStmt->trigname = NameStr(trigForm->tgname);
trigStmt->relation = NULL;
+ trigStmt->relOid = RelationGetRelid(partition);
trigStmt->funcname = NULL; /* passed separately */
trigStmt->args = trigargs;
trigStmt->row = true;
@@ -20917,7 +20921,7 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
trigStmt->initdeferred = trigForm->tginitdeferred;
trigStmt->constrrel = NULL; /* passed separately */
- CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition),
+ CreateTriggerFiringOn(trigStmt, NULL,
trigForm->tgconstrrelid, InvalidOid, InvalidOid,
trigForm->tgfoid, trigForm->oid, qual,
false, true, trigForm->tgenabled);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 12c97f2c023..6e079213863 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -111,9 +111,6 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
* queryString is the source text of the CREATE TRIGGER command.
* This must be supplied if a whenClause is specified, else it can be NULL.
*
- * relOid, if nonzero, is the relation on which the trigger should be
- * created. If zero, the name provided in the statement will be looked up.
- *
* refRelOid, if nonzero, is the relation to which the constraint trigger
* refers. If zero, the constraint relation name provided in the statement
* will be looked up as needed.
@@ -158,12 +155,12 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
*/
ObjectAddress
CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
- Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
+ Oid refRelOid, Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition)
{
return
- CreateTriggerFiringOn(stmt, queryString, relOid, refRelOid,
+ CreateTriggerFiringOn(stmt, queryString, refRelOid,
constraintOid, indexOid, funcoid,
parentTriggerOid, whenClause, isInternal,
in_partition, TRIGGER_FIRES_ON_ORIGIN);
@@ -175,7 +172,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
*/
ObjectAddress
CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
- Oid relOid, Oid refRelOid, Oid constraintOid,
+ Oid refRelOid, Oid constraintOid,
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when)
@@ -208,8 +205,8 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
bool existing_isInternal = false;
bool existing_isClone = false;
- if (OidIsValid(relOid))
- rel = table_open(relOid, ShareRowExclusiveLock);
+ if (OidIsValid(stmt->relOid))
+ rel = table_open(stmt->relOid, ShareRowExclusiveLock);
else
rel = table_openrv(stmt->relation, ShareRowExclusiveLock);
@@ -1186,8 +1183,9 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
map_partition_varattnos((List *) qual, PRS2_NEW_VARNO,
childTbl, rel);
+ childStmt->relOid = partdesc->oids[i];
CreateTriggerFiringOn(childStmt, queryString,
- partdesc->oids[i], refRelOid,
+ refRelOid,
InvalidOid, InvalidOid,
funcoid, trigoid, qual,
isInternal, true, trigger_fires_when);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 28f4e11e30f..01e66ba615e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -6139,6 +6139,7 @@ CreateTrigStmt:
n->isconstraint = false;
n->trigname = $4;
n->relation = $8;
+ n->relOid = InvalidOid;
n->funcname = $14;
n->args = $16;
n->row = $10;
@@ -6188,6 +6189,7 @@ CreateTrigStmt:
n->isconstraint = true;
n->trigname = $5;
n->relation = $9;
+ n->relOid = InvalidOid;
n->funcname = $18;
n->args = $20;
n->row = true;
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index d18a3a60a46..7d6387ad9b5 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1701,7 +1701,7 @@ ProcessUtilitySlow(ParseState *pstate,
case T_CreateTrigStmt:
address = CreateTrigger((CreateTrigStmt *) parsetree,
- queryString, InvalidOid, InvalidOid,
+ queryString, InvalidOid,
InvalidOid, InvalidOid, InvalidOid,
InvalidOid, NULL, false, false);
break;
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index cfd7daa20ed..ae7abd456bf 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -152,11 +152,11 @@ extern PGDLLIMPORT int SessionReplicationRole;
#define TRIGGER_DISABLED 'D'
extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
- Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
+ Oid refRelOid, Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition);
extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
- Oid relOid, Oid refRelOid, Oid constraintOid,
+ Oid refRelOid, Oid constraintOid,
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index bc7adba4a0f..f876f00ab9f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3138,6 +3138,17 @@ typedef struct CreateTrigStmt
bool isconstraint; /* This is a constraint trigger */
char *trigname; /* TRIGGER's name */
RangeVar *relation; /* relation trigger is on */
+
+ /*
+ * The OID of the relation on which the trigger is to be created. If this
+ * is InvalidOid, CreateTrigStmt.relation is used to perform the lookup;
+ * otherwise, use this directly. This is useful when CreateTrigger is
+ * invoked indirectly rather than directly from the parser.
+ *
+ * Using the OID also avoids repeated relation name lookups.
+ */
+ Oid relOid;
+
List *funcname; /* qual. name of function to call */
List *args; /* list of String or NIL */
bool row; /* ROW/STATEMENT */
--
2.34.1
v4-0002-add-constrrelOid-field-to-CreateTrigStmt.patchtext/x-patch; charset=US-ASCII; name=v4-0002-add-constrrelOid-field-to-CreateTrigStmt.patchDownload
From fe2aa96bdded441bc266d3e3221cdbb531c369a7 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Sat, 27 Dec 2025 13:58:11 +0800
Subject: [PATCH v4 2/3] add constrrelOid field to CreateTrigStmt
In the spirit of the change made to add the relOid field to CreateTrigStmt.
discussion: https://postgr.es/m/CACJufxHJAr2FjbeB6ghg_-N5dxX5JVnjKSLOUxOyt4TeaAWQkg@mail.gmail.com
discussion: https://postgr.es/m/CACJufxGkqYrmwMdvUOUPet0443oUTgF_dKCpw3TfJiutfuywAQ@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/6087
commitfest: https://commitfest.postgresql.org/patch/6089
---
src/backend/catalog/index.c | 3 ++-
src/backend/commands/tablecmds.c | 13 ++++++++-----
src/backend/commands/trigger.c | 18 +++++++-----------
src/backend/parser/gram.y | 2 ++
src/backend/tcop/utility.c | 2 +-
src/include/commands/trigger.h | 4 ++--
src/include/nodes/parsenodes.h | 1 +
7 files changed, 23 insertions(+), 20 deletions(-)
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4afcb2872e7..bb63701fb98 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2038,9 +2038,10 @@ index_constraint_create(Relation heapRelation,
trigger->deferrable = true;
trigger->initdeferred = initdeferred;
trigger->constrrel = NULL;
+ trigger->constrrelOid = InvalidOid;
(void) CreateTrigger(trigger, NULL,
- InvalidOid, conOid, indexRelationId, InvalidOid,
+ conOid, indexRelationId, InvalidOid,
InvalidOid, NULL, true, false);
}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ee400c1cd58..cae3d5218aa 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13863,8 +13863,9 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->deferrable = fkconstraint->deferrable;
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
+ fk_trigger->constrrelOid = refRelOid;
- trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ trigAddress = CreateTrigger(fk_trigger, NULL,
constraintOid, indexOid, InvalidOid,
parentTrigOid, NULL, true, false);
@@ -13909,6 +13910,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
+ fk_trigger->constrrelOid = myRelOid;
switch (fkconstraint->fk_del_action)
{
@@ -13943,7 +13945,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid,
+ trigAddress = CreateTrigger(fk_trigger, NULL,
constraintOid, indexOid, InvalidOid,
parentDelTrigger, NULL, true, false);
if (deleteTrigOid)
@@ -13970,6 +13972,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
+ fk_trigger->constrrelOid = myRelOid;
switch (fkconstraint->fk_upd_action)
{
@@ -14004,7 +14007,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
break;
}
- trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid,
+ trigAddress = CreateTrigger(fk_trigger, NULL,
constraintOid, indexOid, InvalidOid,
parentUpdTrigger, NULL, true, false);
if (updateTrigOid)
@@ -20920,9 +20923,9 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
trigStmt->deferrable = trigForm->tgdeferrable;
trigStmt->initdeferred = trigForm->tginitdeferred;
trigStmt->constrrel = NULL; /* passed separately */
+ trigStmt->constrrelOid = trigForm->tgconstrrelid;
- CreateTriggerFiringOn(trigStmt, NULL,
- trigForm->tgconstrrelid, InvalidOid, InvalidOid,
+ CreateTriggerFiringOn(trigStmt, NULL, InvalidOid, InvalidOid,
trigForm->tgfoid, trigForm->oid, qual,
false, true, trigForm->tgenabled);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 6e079213863..18cd3cc41b3 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -111,10 +111,6 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
* queryString is the source text of the CREATE TRIGGER command.
* This must be supplied if a whenClause is specified, else it can be NULL.
*
- * refRelOid, if nonzero, is the relation to which the constraint trigger
- * refers. If zero, the constraint relation name provided in the statement
- * will be looked up as needed.
- *
* constraintOid, if nonzero, says that this trigger is being created
* internally to implement that constraint. A suitable pg_depend entry will
* be made to link the trigger to that constraint. constraintOid is zero when
@@ -155,12 +151,12 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
*/
ObjectAddress
CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
- Oid refRelOid, Oid constraintOid, Oid indexOid,
+ Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition)
{
return
- CreateTriggerFiringOn(stmt, queryString, refRelOid,
+ CreateTriggerFiringOn(stmt, queryString,
constraintOid, indexOid, funcoid,
parentTriggerOid, whenClause, isInternal,
in_partition, TRIGGER_FIRES_ON_ORIGIN);
@@ -172,7 +168,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
*/
ObjectAddress
CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
- Oid refRelOid, Oid constraintOid,
+ Oid constraintOid,
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when)
@@ -324,10 +320,10 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
* might end up creating a pg_constraint entry referencing a
* nonexistent table.
*/
- if (OidIsValid(refRelOid))
+ if (OidIsValid(stmt->constrrelOid))
{
- LockRelationOid(refRelOid, AccessShareLock);
- constrrelid = refRelOid;
+ LockRelationOid(stmt->constrrelOid, AccessShareLock);
+ constrrelid = stmt->constrrelOid;
}
else if (stmt->constrrel != NULL)
constrrelid = RangeVarGetRelid(stmt->constrrel, AccessShareLock,
@@ -1184,8 +1180,8 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
childTbl, rel);
childStmt->relOid = partdesc->oids[i];
+ childStmt->constrrelOid = stmt->constrrelOid;
CreateTriggerFiringOn(childStmt, queryString,
- refRelOid,
InvalidOid, InvalidOid,
funcoid, trigoid, qual,
isInternal, true, trigger_fires_when);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 01e66ba615e..4a0bab33e70 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -6151,6 +6151,7 @@ CreateTrigStmt:
n->deferrable = false;
n->initdeferred = false;
n->constrrel = NULL;
+ n->constrrelOid = InvalidOid;
$$ = (Node *) n;
}
| CREATE opt_or_replace CONSTRAINT TRIGGER name AFTER TriggerEvents ON
@@ -6202,6 +6203,7 @@ CreateTrigStmt:
&n->deferrable, &n->initdeferred, &dummy,
NULL, NULL, yyscanner);
n->constrrel = $10;
+ n->constrrelOid = InvalidOid;
$$ = (Node *) n;
}
;
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 7d6387ad9b5..6c3642e00bc 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1701,7 +1701,7 @@ ProcessUtilitySlow(ParseState *pstate,
case T_CreateTrigStmt:
address = CreateTrigger((CreateTrigStmt *) parsetree,
- queryString, InvalidOid,
+ queryString,
InvalidOid, InvalidOid, InvalidOid,
InvalidOid, NULL, false, false);
break;
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index ae7abd456bf..a2b69576093 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -152,11 +152,11 @@ extern PGDLLIMPORT int SessionReplicationRole;
#define TRIGGER_DISABLED 'D'
extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
- Oid refRelOid, Oid constraintOid, Oid indexOid,
+ Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition);
extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
- Oid refRelOid, Oid constraintOid,
+ Oid constraintOid,
Oid indexOid, Oid funcoid, Oid parentTriggerOid,
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f876f00ab9f..a3bf11e4fb7 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3164,6 +3164,7 @@ typedef struct CreateTrigStmt
bool deferrable; /* [NOT] DEFERRABLE */
bool initdeferred; /* INITIALLY {DEFERRED|IMMEDIATE} */
RangeVar *constrrel; /* opposite relation, if RI trigger */
+ Oid constrrelOid; /* opposite relation Oid, if RI trigger */
} CreateTrigStmt;
/* ----------------------
--
2.34.1
v4-0003-CREATE-TABLE-LIKE-INCLUDING-TRIGGERS.patchtext/x-patch; charset=US-ASCII; name=v4-0003-CREATE-TABLE-LIKE-INCLUDING-TRIGGERS.patchDownload
From 654df7b3c03701e292e3af0f056f02ee49b010df Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Wed, 31 Dec 2025 11:18:15 +0800
Subject: [PATCH v4 3/3] CREATE TABLE LIKE INCLUDING TRIGGERS
This will copy all source table's trigger to the new table. Internal trigger
(such as foreign key associated trigger) won't being copied to new table.
However this command will fail if the source table's trigger contain whole-row
reference.
Trigger's comment will copied to new table, if INCLUDING COMMENTS is specified.
CREATE FOREIGN TABLE LIKE is also supported.
discussion: https://postgr.es/m/CACJufxHJAr2FjbeB6ghg_-N5dxX5JVnjKSLOUxOyt4TeaAWQkg@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/6087
---
doc/src/sgml/ref/create_foreign_table.sgml | 11 +-
doc/src/sgml/ref/create_table.sgml | 13 +-
src/backend/catalog/index.c | 2 +
src/backend/commands/tablecmds.c | 8 +
src/backend/commands/trigger.c | 212 +++++++++++++++++-
src/backend/parser/gram.y | 9 +-
src/backend/parser/parse_utilcmd.c | 43 +++-
src/include/commands/trigger.h | 3 +-
src/include/nodes/parsenodes.h | 4 +
src/include/parser/kwlist.h | 1 +
.../regress/expected/create_table_like.out | 6 +
src/test/regress/expected/triggers.out | 123 ++++++++++
src/test/regress/sql/create_table_like.sql | 4 +
src/test/regress/sql/triggers.sql | 51 +++++
14 files changed, 477 insertions(+), 13 deletions(-)
diff --git a/doc/src/sgml/ref/create_foreign_table.sgml b/doc/src/sgml/ref/create_foreign_table.sgml
index 08a8ceeae75..68d0608b0a3 100644
--- a/doc/src/sgml/ref/create_foreign_table.sgml
+++ b/doc/src/sgml/ref/create_foreign_table.sgml
@@ -60,7 +60,7 @@ CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
-{ INCLUDING | EXCLUDING } { COMMENTS | CONSTRAINTS | DEFAULTS | GENERATED | STATISTICS | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | CONSTRAINTS | DEFAULTS | GENERATED | STATISTICS | TRIGGERS | ALL }
<phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
@@ -284,6 +284,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><literal>INCLUDING TRIGGERS</literal></term>
+ <listitem>
+ <para>
+ All non-internal triggers are copied to the new table.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>INCLUDING ALL</literal></term>
<listitem>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 77c5a763d45..c6a0f6dae9b 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -88,7 +88,7 @@ class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable cl
<phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | TRIGGERS | ALL }
<phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
@@ -672,7 +672,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>INCLUDING COMMENTS</literal></term>
<listitem>
<para>
- Comments for the copied columns, constraints, and indexes will be
+ Comments for the copied columns, constraints, indexes and triggers will be
copied. The default behavior is to exclude comments, resulting in
the copied columns and constraints in the new table having no
comments.
@@ -776,6 +776,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-like-opt-triggers">
+ <term><literal>INCLUDING TRIGGERS</literal></term>
+ <listitem>
+ <para>
+ All non-internal triggers on the original table will be created on the new table.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-parms-like-opt-all">
<term><literal>INCLUDING ALL</literal></term>
<listitem>
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index bb63701fb98..8175ba5f2c5 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2039,6 +2039,8 @@ index_constraint_create(Relation heapRelation,
trigger->initdeferred = initdeferred;
trigger->constrrel = NULL;
trigger->constrrelOid = InvalidOid;
+ trigger->trigcomment = NULL;
+ trigger->transformed = true;
(void) CreateTrigger(trigger, NULL,
conOid, indexRelationId, InvalidOid,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cae3d5218aa..78ef3935d0a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13864,6 +13864,8 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
fk_trigger->constrrelOid = refRelOid;
+ fk_trigger->trigcomment = NULL;
+ fk_trigger->transformed = true;
trigAddress = CreateTrigger(fk_trigger, NULL,
constraintOid, indexOid, InvalidOid,
@@ -13911,6 +13913,8 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
fk_trigger->constrrelOid = myRelOid;
+ fk_trigger->trigcomment = NULL;
+ fk_trigger->transformed = true;
switch (fkconstraint->fk_del_action)
{
@@ -13973,6 +13977,8 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
fk_trigger->constrrelOid = myRelOid;
+ fk_trigger->trigcomment = NULL;
+ fk_trigger->transformed = true;
switch (fkconstraint->fk_upd_action)
{
@@ -20924,6 +20930,8 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
trigStmt->initdeferred = trigForm->tginitdeferred;
trigStmt->constrrel = NULL; /* passed separately */
trigStmt->constrrelOid = trigForm->tgconstrrelid;
+ trigStmt->trigcomment = NULL;
+ trigStmt->transformed = true;
CreateTriggerFiringOn(trigStmt, NULL, InvalidOid, InvalidOid,
trigForm->tgfoid, trigForm->oid, qual,
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 18cd3cc41b3..f91f21ec34c 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -30,6 +30,7 @@
#include "catalog/pg_proc.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
+#include "commands/comment.h"
#include "commands/trigger.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -582,13 +583,20 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
false, false);
addNSItemToQuery(pstate, nsitem, false, true, true);
- /* Transform expression. Copy to be sure we don't modify original */
- whenClause = transformWhereClause(pstate,
- copyObject(stmt->whenClause),
- EXPR_KIND_TRIGGER_WHEN,
- "WHEN");
- /* we have to fix its collations too */
- assign_expr_collations(pstate, whenClause);
+ if (stmt->transformed)
+ whenClause = stmt->whenClause;
+ else
+ {
+ /* Transform expression. Copy to be sure we don't modify original */
+ whenClause = transformWhereClause(pstate,
+ copyObject(stmt->whenClause),
+ EXPR_KIND_TRIGGER_WHEN,
+ "WHEN");
+ /* we have to fix its collations too */
+ assign_expr_collations(pstate, whenClause);
+
+ stmt->transformed = true;
+ }
/*
* Check for disallowed references to OLD/NEW.
@@ -1198,6 +1206,11 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
/* Keep lock on target rel until end of xact */
table_close(rel, NoLock);
+ /* Add any requested comment */
+ if (stmt->trigcomment != NULL)
+ CreateComments(trigoid, TriggerRelationId, 0,
+ stmt->trigcomment);
+
return myself;
}
@@ -6709,3 +6722,188 @@ check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple tuple)
return tuple;
}
+
+/*
+ * Copy trigger definition to the new relation: Populate CreateTrigStmt using
+ * the existing trigger (source_trigid on source_rel) for the target relation
+ * (heapRel).
+ *
+ * Attribute numbers in expression Vars are adjusted according to attmap.
+ */
+void
+generateClonedTriggerStmt(CreateTrigStmt *trigStmt, RangeVar *heapRel, Oid source_trigid,
+ Relation source_rel, const AttrMap *attmap)
+{
+ HeapTuple triggerTuple;
+ HeapTuple proctup;
+ Form_pg_trigger trigForm;
+ Form_pg_proc procform;
+ Relation pg_trigger;
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Datum value;
+ bool isnull;
+ Node *qual = NULL;
+ List *trigargs = NIL;
+ List *cols = NIL;
+ List *funcname = NIL;
+ List *transitionRels = NIL;
+ char *schemaname;
+
+ pg_trigger = table_open(TriggerRelationId, AccessShareLock);
+
+ /* Find the trigger to copy */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(source_trigid));
+
+ tgscan = systable_beginscan(pg_trigger, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ triggerTuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(triggerTuple))
+ elog(ERROR, "could not find tuple for trigger %u", source_trigid);
+
+ trigForm = (Form_pg_trigger) GETSTRUCT(triggerTuple);
+
+ Assert(!trigForm->tgisinternal);
+
+ /* Reconstruct trigger function String list */
+ proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(trigForm->tgfoid));
+ if (!HeapTupleIsValid(proctup))
+ elog(ERROR, "cache lookup failed for function %u", trigForm->tgfoid);
+ procform = (Form_pg_proc) GETSTRUCT(proctup);
+
+ schemaname = get_namespace_name(procform->pronamespace);
+ funcname = list_make2(makeString(schemaname),
+ makeString(NameStr(procform->proname)));
+ ReleaseSysCache(proctup);
+
+ /* Reconstruct trigger arguments list */
+ if (trigForm->tgnargs > 0)
+ {
+ bytea *val;
+ char *p;
+
+ val = DatumGetByteaPP(fastgetattr(triggerTuple,
+ Anum_pg_trigger_tgargs,
+ RelationGetDescr(pg_trigger),
+ &isnull));
+ if (isnull)
+ elog(ERROR, "tgargs is null in trigger \"%s\" for relation \"%s\"",
+ NameStr(trigForm->tgname),
+ RelationGetRelationName(source_rel));
+
+ p = (char *) VARDATA_ANY(val);
+
+ for (int i = 0; i < trigForm->tgnargs; i++)
+ {
+ trigargs = lappend(trigargs, makeString(pstrdup(p)));
+ p += strlen(p) + 1;
+ }
+ }
+
+ /*
+ * If there is a column list, transform it to a list of column names. Note
+ * we don't need to map this list in any way ...
+ */
+ if (trigForm->tgattr.dim1 > 0)
+ {
+ for (int i = 0; i < trigForm->tgattr.dim1; i++)
+ {
+ Form_pg_attribute col;
+
+ col = TupleDescAttr(RelationGetDescr(source_rel),
+ trigForm->tgattr.values[i] - 1);
+ cols = lappend(cols,
+ makeString(pstrdup(NameStr(col->attname))));
+ }
+ }
+
+ /* If the trigger has a WHEN qualification, add that */
+ value = fastgetattr(triggerTuple, Anum_pg_trigger_tgqual,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (!isnull)
+ {
+ bool found_whole_row;
+
+ qual = stringToNode(TextDatumGetCString(value));
+
+ /* Adjust Vars to match new table's column numbering */
+ qual = map_variable_attnos(qual, PRS2_NEW_VARNO, 0,
+ attmap,
+ InvalidOid,
+ &found_whole_row);
+
+ /* As in expandTableLikeClause, reject whole-row variables */
+ if (found_whole_row)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot convert whole-row table reference"),
+ errdetail("Trigger \"%s\" contains a whole-row table reference.",
+ NameStr(trigForm->tgname)));
+
+ qual = map_variable_attnos(qual, PRS2_OLD_VARNO, 0,
+ attmap,
+ InvalidOid,
+ &found_whole_row);
+
+ /* As in expandTableLikeClause, reject whole-row variables */
+ if (found_whole_row)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot convert whole-row table reference"),
+ errdetail("Trigger \"%s\" contains a whole-row table reference.",
+ NameStr(trigForm->tgname)));
+ }
+
+ /* Reconstruct trigger old transition table */
+ value = fastgetattr(triggerTuple, Anum_pg_trigger_tgoldtable,
+ RelationGetDescr(pg_trigger), &isnull);
+ if (!isnull)
+ {
+ TriggerTransition *old = makeNode(TriggerTransition);
+
+ old->isNew = false;
+ old->name = pstrdup(NameStr(*DatumGetName(value)));
+ old->isTable = true;
+ transitionRels = lappend(transitionRels, old);
+ }
+
+ /* Reconstruct trigger old transition table */
+ value = fastgetattr(triggerTuple, Anum_pg_trigger_tgnewtable,
+ RelationGetDescr(pg_trigger),
+ &isnull);
+ if (!isnull)
+ {
+ TriggerTransition *new = makeNode(TriggerTransition);
+
+ new->isNew = true;
+ new->name = pstrdup(NameStr(*DatumGetName(value)));
+ new->isTable = true;
+ transitionRels = lappend(transitionRels, new);
+ }
+
+ trigStmt->replace = false;
+ trigStmt->isconstraint = OidIsValid(trigForm->tgconstraint);
+ trigStmt->trigname = pstrdup(NameStr(trigForm->tgname));
+ trigStmt->relation = heapRel;
+ Assert(OidIsValid(trigStmt->relOid));
+ trigStmt->funcname = funcname;
+ trigStmt->args = trigargs;
+ trigStmt->row = TRIGGER_FOR_ROW(trigForm->tgtype);
+ trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK;
+ trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK;
+ trigStmt->columns = cols;
+ trigStmt->whenClause = qual;
+ trigStmt->transitionRels = transitionRels;
+ trigStmt->deferrable = trigForm->tgdeferrable;
+ trigStmt->initdeferred = trigForm->tginitdeferred;
+ trigStmt->constrrelOid = trigForm->tgconstrrelid;
+ trigStmt->trigcomment = NULL;
+ trigStmt->transformed = true;
+
+ systable_endscan(tgscan);
+ table_close(pg_trigger, AccessShareLock);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4a0bab33e70..50fc189613e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -786,7 +786,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
TABLE TABLES TABLESAMPLE TABLESPACE TARGET TEMP TEMPLATE TEMPORARY TEXT_P THEN
TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSFORM
- TREAT TRIGGER TRIM TRUE_P
+ TREAT TRIGGER TRIGGERS TRIM TRUE_P
TRUNCATE TRUSTED TYPE_P TYPES_P
UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN
@@ -4296,6 +4296,7 @@ TableLikeOption:
| INDEXES { $$ = CREATE_TABLE_LIKE_INDEXES; }
| STATISTICS { $$ = CREATE_TABLE_LIKE_STATISTICS; }
| STORAGE { $$ = CREATE_TABLE_LIKE_STORAGE; }
+ | TRIGGERS { $$ = CREATE_TABLE_LIKE_TRIGGERS; }
| ALL { $$ = CREATE_TABLE_LIKE_ALL; }
;
@@ -6152,6 +6153,8 @@ CreateTrigStmt:
n->initdeferred = false;
n->constrrel = NULL;
n->constrrelOid = InvalidOid;
+ n->trigcomment = NULL;
+ n->transformed = false;
$$ = (Node *) n;
}
| CREATE opt_or_replace CONSTRAINT TRIGGER name AFTER TriggerEvents ON
@@ -6204,6 +6207,8 @@ CreateTrigStmt:
NULL, NULL, yyscanner);
n->constrrel = $10;
n->constrrelOid = InvalidOid;
+ n->trigcomment = NULL;
+ n->transformed = false;
$$ = (Node *) n;
}
;
@@ -18193,6 +18198,7 @@ unreserved_keyword:
| TRANSACTION
| TRANSFORM
| TRIGGER
+ | TRIGGERS
| TRUNCATE
| TRUSTED
| TYPE_P
@@ -18846,6 +18852,7 @@ bare_label_keyword:
| TRANSFORM
| TREAT
| TRIGGER
+ | TRIGGERS
| TRIM
| TRUE_P
| TRUNCATE
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 2b7b084f216..8d36e0b4ae2 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -39,12 +39,14 @@
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_trigger.h"
#include "catalog/pg_type.h"
#include "commands/comment.h"
#include "commands/defrem.h"
#include "commands/sequence.h"
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
+#include "commands/trigger.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
@@ -1321,7 +1323,8 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
CREATE_TABLE_LIKE_GENERATED |
CREATE_TABLE_LIKE_CONSTRAINTS |
CREATE_TABLE_LIKE_INDEXES |
- CREATE_TABLE_LIKE_STATISTICS))
+ CREATE_TABLE_LIKE_STATISTICS |
+ CREATE_TABLE_LIKE_TRIGGERS))
{
table_like_clause->relationOid = RelationGetRelid(relation);
cxt->likeclauses = lappend(cxt->likeclauses, table_like_clause);
@@ -1587,6 +1590,44 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
}
}
+ /* Process triggers if required */
+ if ((table_like_clause->options & CREATE_TABLE_LIKE_TRIGGERS) &&
+ relation->trigdesc != NULL)
+ {
+ bool include_comments;
+ CreateTrigStmt *trig_stmt = NULL;
+
+ include_comments = (table_like_clause->options & CREATE_TABLE_LIKE_COMMENTS);
+
+ for (int nt = 0; nt < relation->trigdesc->numtriggers; nt++)
+ {
+ Trigger *trig = relation->trigdesc->triggers + nt;
+ Oid trigoid = trig->tgoid;
+
+ /* internal trigger won't copied to new table */
+ if (trig->tgisinternal)
+ continue;
+
+ trig_stmt = makeNode(CreateTrigStmt);
+
+ trig_stmt->relOid = RelationGetRelid(childrel);
+
+ generateClonedTriggerStmt(trig_stmt, heapRel, trigoid, relation,
+ attmap);
+
+ /* Copy comment on trigger, if requested */
+ if (include_comments)
+ {
+ comment = GetComment(trigoid, TriggerRelationId, 0);
+
+ /* We make use of CreateTrigStmt's trigcomment option */
+ trig_stmt->trigcomment = comment;
+ }
+
+ result = lappend(result, trig_stmt);
+ }
+ }
+
/*
* Process extended statistics if required.
*/
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index a2b69576093..e8494ad9e0a 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -266,7 +266,8 @@ extern void AfterTriggerBeginSubXact(void);
extern void AfterTriggerEndSubXact(bool isCommit);
extern void AfterTriggerSetState(ConstraintsSetStmt *stmt);
extern bool AfterTriggerPendingOnRel(Oid relid);
-
+extern void generateClonedTriggerStmt(CreateTrigStmt *trigStmt, RangeVar *heapRel, Oid source_trigid,
+ Relation source_rel, const AttrMap *attmap);
/*
* in utils/adt/ri_triggers.c
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index a3bf11e4fb7..ded02f81ba1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -795,6 +795,7 @@ typedef enum TableLikeOption
CREATE_TABLE_LIKE_INDEXES = 1 << 6,
CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+ CREATE_TABLE_LIKE_TRIGGERS = 1 << 9,
CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
} TableLikeOption;
@@ -3165,6 +3166,9 @@ typedef struct CreateTrigStmt
bool initdeferred; /* INITIALLY {DEFERRED|IMMEDIATE} */
RangeVar *constrrel; /* opposite relation, if RI trigger */
Oid constrrelOid; /* opposite relation Oid, if RI trigger */
+ char *trigcomment; /* comment to apply to trigger, or NULL */
+ bool transformed; /* true means no need do parse analysis for
+ * whenClause */
} CreateTrigStmt;
/* ----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 9fde58f541c..479a157332c 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -464,6 +464,7 @@ PG_KEYWORD("transaction", TRANSACTION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("transform", TRANSFORM, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("treat", TREAT, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("trigger", TRIGGER, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("triggers", TRIGGERS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("trim", TRIM, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("true", TRUE_P, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("truncate", TRUNCATE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index d3c35c14847..8451bccbd13 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -631,6 +631,10 @@ Not-null constraints:
"ctl_table_a_not_null" NOT NULL "a"
"ctl_table_d_not_null" NOT NULL "d"
+-- trigger_func was created in triggers.sql
+CREATE TRIGGER trigtest_before_stmt BEFORE DELETE OR UPDATE ON ctl_table
+FOR EACH ROW WHEN (OLD.a > 0)
+EXECUTE PROCEDURE trigger_func('trigtest_before_stmt');
-- Test EXCLUDING ALL
CREATE FOREIGN TABLE ctl_foreign_table1(LIKE ctl_table EXCLUDING ALL) SERVER ctl_s0;
\d+ ctl_foreign_table1
@@ -679,6 +683,8 @@ Statistics objects:
Not-null constraints:
"ctl_table_a_not_null" NOT NULL "a"
"ctl_table_d_not_null" NOT NULL "d"
+Triggers:
+ trigtest_before_stmt BEFORE DELETE OR UPDATE ON ctl_foreign_table2 FOR EACH ROW WHEN (old.a > 0) EXECUTE FUNCTION trigger_func('trigtest_before_stmt')
Server: ctl_s0
-- \d+ does not report the value of attcompression for a foreign table, so
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 1eb8fba0953..089c051eec3 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -165,6 +165,7 @@ drop table trigtest;
CREATE TABLE log_table (tstamp timestamp default timeofday()::timestamp);
CREATE TABLE main_table (a int unique, b int);
COPY main_table (a,b) FROM stdin;
+-- This function also used in create_table_like.sql, don't drop it.
CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS '
BEGIN
RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
@@ -174,6 +175,20 @@ CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_table
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt');
CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt');
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW
+WHEN (OLD is not null)
+EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+ERROR: cannot convert whole-row table reference
+DETAIL: Trigger "wholetrig" contains a whole-row table reference.
+DROP TRIGGER wholetrig ON main_table;
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW
+WHEN (NEW is not null)
+EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+ERROR: cannot convert whole-row table reference
+DETAIL: Trigger "wholetrig" contains a whole-row table reference.
+DROP TRIGGER wholetrig ON main_table;
--
-- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified,
-- CREATE TRIGGER should default to 'FOR EACH STATEMENT'
@@ -394,6 +409,53 @@ NOTICE: trigger_func(after_upd_a_b_row) called: action = UPDATE, when = AFTER,
NOTICE: trigger_func(after_upd_b_row) called: action = UPDATE, when = AFTER, level = ROW
NOTICE: trigger_func(after_upd_b_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
NOTICE: trigger_func(after_upd_stmt) called: action = UPDATE, when = AFTER, level = STATEMENT
+--create table like tests
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS 'trigger before_ins_stmt_trig';
+CREATE TABLE main_table1(c INT, LIKE main_table INCLUDING TRIGGERS INCLUDING COMMENTS);
+\d main_table
+ Table "public.main_table"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ after_ins_stmt_trig AFTER INSERT ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_ins_stmt')
+ after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_a_b_row')
+ after_upd_b_row_trig AFTER UPDATE OF b ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_b_row')
+ after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_b_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_ins_stmt')
+ before_upd_a_row_trig BEFORE UPDATE OF a ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('before_upd_a_row')
+ before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_upd_a_stmt')
+
+\d main_table1
+ Table "public.main_table1"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ c | integer | | |
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ after_ins_stmt_trig AFTER INSERT ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_ins_stmt')
+ after_upd_a_b_row_trig AFTER UPDATE OF a, b ON main_table1 FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_a_b_row')
+ after_upd_b_row_trig AFTER UPDATE OF b ON main_table1 FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_b_row')
+ after_upd_b_stmt_trig AFTER UPDATE OF b ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_b_stmt')
+ after_upd_stmt_trig AFTER UPDATE ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_stmt')
+ before_ins_stmt_trig BEFORE INSERT ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_ins_stmt')
+ before_upd_a_row_trig BEFORE UPDATE OF a ON main_table1 FOR EACH ROW EXECUTE FUNCTION trigger_func('before_upd_a_row')
+ before_upd_a_stmt_trig BEFORE UPDATE OF a ON main_table1 FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_upd_a_stmt')
+
+SELECT pc.relname, pd.tgname, obj_description(pd.oid, 'pg_trigger')
+ FROM pg_trigger pd JOIN pg_class pc ON (pc.oid = pd.tgrelid)
+ AND pd.tgname = 'before_ins_stmt_trig'
+ ORDER BY 1;
+ relname | tgname | obj_description
+-------------+----------------------+------------------------------
+ main_table | before_ins_stmt_trig | trigger before_ins_stmt_trig
+ main_table1 | before_ins_stmt_trig | trigger before_ins_stmt_trig
+(2 rows)
+
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS NULL;
--
-- Test case for bug with BEFORE trigger followed by AFTER trigger with WHEN
--
@@ -422,6 +484,20 @@ NOTICE: dummy_update_func(afterb) called: action = UPDATE, old = (t), new = (f)
UPDATE some_t SET some_col = TRUE;
NOTICE: dummy_update_func(before) called: action = UPDATE, old = (f), new = (t)
NOTICE: dummy_update_func(aftera) called: action = UPDATE, old = (f), new = (t)
+CREATE TABLE some_t1 (c INT, LIKE some_t INCLUDING TRIGGERS INCLUDING COMMENTS);
+\d+ some_t1
+ Table "public.some_t1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+----------+---------+-----------+----------+---------+---------+--------------+-------------
+ c | integer | | | | plain | |
+ some_col | boolean | | not null | | plain | |
+Not-null constraints:
+ "some_t_some_col_not_null" NOT NULL "some_col"
+Triggers:
+ some_trig_aftera AFTER UPDATE ON some_t1 FOR EACH ROW WHEN (NOT old.some_col AND new.some_col) EXECUTE FUNCTION dummy_update_func('aftera')
+ some_trig_afterb AFTER UPDATE ON some_t1 FOR EACH ROW WHEN (NOT new.some_col) EXECUTE FUNCTION dummy_update_func('afterb')
+ some_trig_before BEFORE UPDATE ON some_t1 FOR EACH ROW EXECUTE FUNCTION dummy_update_func('before')
+
DROP TABLE some_t;
-- bogus cases
CREATE TRIGGER error_upd_and_col BEFORE UPDATE OR UPDATE OF a ON main_table
@@ -861,6 +937,9 @@ CREATE TRIGGER instead_of_update_trig INSTEAD OF UPDATE ON main_view
FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+CREATE TABLE main_view_table(LIKE main_view INCLUDING TRIGGERS); --error
+ERROR: "main_view_table" is a table
+DETAIL: Tables cannot have INSTEAD OF triggers.
-- Valid BEFORE statement VIEW triggers
CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
@@ -2318,6 +2397,20 @@ create constraint trigger parted_trig_two after insert on parted_constr
deferrable initially deferred enforced
for each row when (bark(new.b) AND new.a % 2 = 1)
execute procedure trigger_notice_ab();
+create table parted_constr_copy (like parted_constr including all);
+select pg_get_triggerdef(oid)
+from pg_trigger
+where not tgisinternal and tgrelid in ('parted_constr_copy'::regclass, 'parted_constr'::regclass)
+order by tgname;
+ pg_get_triggerdef
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE CONSTRAINT TRIGGER parted_trig AFTER INSERT ON public.parted_constr DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE FUNCTION trigger_notice_ab()
+ CREATE CONSTRAINT TRIGGER parted_trig AFTER INSERT ON public.parted_constr_copy DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE FUNCTION trigger_notice_ab()
+ CREATE CONSTRAINT TRIGGER parted_trig_two AFTER INSERT ON public.parted_constr DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN ((bark(new.b) AND ((new.a % 2) = 1))) EXECUTE FUNCTION trigger_notice_ab()
+ CREATE CONSTRAINT TRIGGER parted_trig_two AFTER INSERT ON public.parted_constr_copy DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN ((bark(new.b) AND ((new.a % 2) = 1))) EXECUTE FUNCTION trigger_notice_ab()
+(4 rows)
+
+drop table parted_constr_copy;
-- The immediate constraint is fired immediately; the WHEN clause of the
-- deferred constraint is also called immediately. The deferred constraint
-- is fired at commit time.
@@ -3092,6 +3185,18 @@ create trigger iocdu_tt_parted_insert_trig
create trigger iocdu_tt_parted_update_trig
after update on iocdu_tt_parted referencing old table as old_table new table as new_table
for each statement execute procedure dump_update();
+CREATE TABLE iocdu_tt_parted_copy(LIKE iocdu_tt_parted INCLUDING TRIGGERS);
+\d iocdu_tt_parted_copy
+ Table "public.iocdu_tt_parted_copy"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | not null |
+ b | text | | |
+Triggers:
+ iocdu_tt_parted_insert_trig AFTER INSERT ON iocdu_tt_parted_copy REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION dump_insert()
+ iocdu_tt_parted_update_trig AFTER UPDATE ON iocdu_tt_parted_copy REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION dump_update()
+
+DROP TABLE iocdu_tt_parted_copy;
-- inserts only
insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB')
on conflict (a) do
@@ -3574,6 +3679,24 @@ begin
end;
$$;
alter function whoami() owner to regress_fn_owner;
+--CREATE TABLE LIKE INCLUDING TRIGGERS
+-- test constraint trigger that reference another table
+create table trig_t1 (id integer);
+create table trig_t2 (id integer);
+create constraint trigger con_trig_test after insert on trig_t1 from trig_t2
+ deferrable initially deferred
+ for each row
+ execute function whoami();
+create table trig_t1_copy(like trig_t1 including triggers);
+\d trig_t1_copy
+ Table "public.trig_t1_copy"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | |
+Triggers:
+ con_trig_test AFTER INSERT ON trig_t1_copy FROM trig_t2 DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION whoami()
+
+drop table trig_t1, trig_t2, trig_t1_copy;
create table defer_trig (id integer);
grant insert on defer_trig to public;
create constraint trigger whoami after insert on defer_trig
diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql
index 93389b57dbf..34ceeb76d92 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -258,6 +258,10 @@ ALTER TABLE ctl_table ADD CONSTRAINT foo CHECK (b = 'text');
ALTER TABLE ctl_table ALTER COLUMN b SET STORAGE MAIN;
\d+ ctl_table
+-- trigger_func was created in triggers.sql
+CREATE TRIGGER trigtest_before_stmt BEFORE DELETE OR UPDATE ON ctl_table
+FOR EACH ROW WHEN (OLD.a > 0)
+EXECUTE PROCEDURE trigger_func('trigtest_before_stmt');
-- Test EXCLUDING ALL
CREATE FOREIGN TABLE ctl_foreign_table1(LIKE ctl_table EXCLUDING ALL) SERVER ctl_s0;
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 5f7f75d7ba5..bf56d18c870 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -108,6 +108,7 @@ COPY main_table (a,b) FROM stdin;
80 15
\.
+-- This function also used in create_table_like.sql, don't drop it.
CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS '
BEGIN
RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
@@ -120,6 +121,18 @@ FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('before_ins_stmt');
CREATE TRIGGER after_ins_stmt_trig AFTER INSERT ON main_table
FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func('after_ins_stmt');
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW
+WHEN (OLD is not null)
+EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+DROP TRIGGER wholetrig ON main_table;
+
+CREATE TRIGGER wholetrig BEFORE UPDATE ON main_table FOR EACH ROW
+WHEN (NEW is not null)
+EXECUTE PROCEDURE trigger_func('modified_a');
+CREATE TABLE main_table1(LIKE main_table INCLUDING TRIGGERS); --error, wholerow reference
+DROP TRIGGER wholetrig ON main_table;
+
--
-- if neither 'FOR EACH ROW' nor 'FOR EACH STATEMENT' was specified,
-- CREATE TRIGGER should default to 'FOR EACH STATEMENT'
@@ -234,6 +247,17 @@ SELECT pg_get_triggerdef(oid) FROM pg_trigger WHERE tgrelid = 'main_table'::regc
UPDATE main_table SET a = 50;
UPDATE main_table SET b = 10;
+--create table like tests
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS 'trigger before_ins_stmt_trig';
+CREATE TABLE main_table1(c INT, LIKE main_table INCLUDING TRIGGERS INCLUDING COMMENTS);
+\d main_table
+\d main_table1
+SELECT pc.relname, pd.tgname, obj_description(pd.oid, 'pg_trigger')
+ FROM pg_trigger pd JOIN pg_class pc ON (pc.oid = pd.tgrelid)
+ AND pd.tgname = 'before_ins_stmt_trig'
+ ORDER BY 1;
+COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS NULL;
+
--
-- Test case for bug with BEFORE trigger followed by AFTER trigger with WHEN
--
@@ -258,6 +282,8 @@ INSERT INTO some_t VALUES (TRUE);
UPDATE some_t SET some_col = TRUE;
UPDATE some_t SET some_col = FALSE;
UPDATE some_t SET some_col = TRUE;
+CREATE TABLE some_t1 (c INT, LIKE some_t INCLUDING TRIGGERS INCLUDING COMMENTS);
+\d+ some_t1
DROP TABLE some_t;
-- bogus cases
@@ -621,6 +647,8 @@ FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_upd');
CREATE TRIGGER instead_of_delete_trig INSTEAD OF DELETE ON main_view
FOR EACH ROW EXECUTE PROCEDURE view_trigger('instead_of_del');
+CREATE TABLE main_view_table(LIKE main_view INCLUDING TRIGGERS); --error
+
-- Valid BEFORE statement VIEW triggers
CREATE TRIGGER before_ins_stmt_trig BEFORE INSERT ON main_view
FOR EACH STATEMENT EXECUTE PROCEDURE view_trigger('before_view_ins_stmt');
@@ -1608,6 +1636,13 @@ create constraint trigger parted_trig_two after insert on parted_constr
for each row when (bark(new.b) AND new.a % 2 = 1)
execute procedure trigger_notice_ab();
+create table parted_constr_copy (like parted_constr including all);
+select pg_get_triggerdef(oid)
+from pg_trigger
+where not tgisinternal and tgrelid in ('parted_constr_copy'::regclass, 'parted_constr'::regclass)
+order by tgname;
+drop table parted_constr_copy;
+
-- The immediate constraint is fired immediately; the WHEN clause of the
-- deferred constraint is also called immediately. The deferred constraint
-- is fired at commit time.
@@ -2278,6 +2313,10 @@ create trigger iocdu_tt_parted_update_trig
after update on iocdu_tt_parted referencing old table as old_table new table as new_table
for each statement execute procedure dump_update();
+CREATE TABLE iocdu_tt_parted_copy(LIKE iocdu_tt_parted INCLUDING TRIGGERS);
+\d iocdu_tt_parted_copy
+DROP TABLE iocdu_tt_parted_copy;
+
-- inserts only
insert into iocdu_tt_parted values (1, 'AAA'), (2, 'BBB')
on conflict (a) do
@@ -2735,6 +2774,18 @@ end;
$$;
alter function whoami() owner to regress_fn_owner;
+--CREATE TABLE LIKE INCLUDING TRIGGERS
+-- test constraint trigger that reference another table
+create table trig_t1 (id integer);
+create table trig_t2 (id integer);
+create constraint trigger con_trig_test after insert on trig_t1 from trig_t2
+ deferrable initially deferred
+ for each row
+ execute function whoami();
+create table trig_t1_copy(like trig_t1 including triggers);
+\d trig_t1_copy
+drop table trig_t1, trig_t2, trig_t1_copy;
+
create table defer_trig (id integer);
grant insert on defer_trig to public;
create constraint trigger whoami after insert on defer_trig
--
2.34.1
Hi Jian!
The feature makes sense from my POV.
On 31 Dec 2025, at 10:08, jian he <jian.universality@gmail.com> wrote:
<v4-0001-add-relOid-field-to-CreateTrigStmt.patch><v4-0002-add-constrrelOid-field-to-CreateTrigStmt.patch>
I'm not an expert in the area, but still decided to review the patch a bit.
First two steps seems to add Oids along with RangeVars. It seems suspicious to me.
Parse Nodes seem to use "textual" identifiers without resolving them to Oids. Oids are specific to session catalog snapshot, but parse nodes
By adding Oid fields we will have to check both RangeVars and Oids all over the code.
Other INCLUDING options (indexes, constraints) don't modify their statement nodes this way - they create fresh nodes with resolved references.
I'm not opposed, but I suggest you to get an opinion of an expert in parse nodes about it, maybe in Discord if this thread does not attract attention. It seems a fundamental stuff for two of your patchsets.
+ char *trigcomment; /* comment to apply to trigger, or NULL */
No other Create*Stmt has a comment field. Comments seem to be handled through separate CommentStmt creation.
Some nitpicking about tests:
1. INSTEAD OF triggers on views - The error is tested, but should also test that statement-level VIEW triggers work
2. Triggers on partitioned tables - What happens when you LIKE a partitioned table? Are partition triggers cloned?
3. Cross-schema trigger functions - The function name reconstruction handles schemas, but is it tested?
+ funcname = list_make2(makeString(schemaname),makeString(NameStr(procform->proname)));
Other NameStr() are pstrdup()'d, maybe let's pstrdup() this too?
+ /* Reconstruct trigger old transition table */
Second instance of this comment is wrong.
+ PG_KEYWORD("triggers", TRIGGERS, UNRESERVED_KEYWORD, BARE_LABEL)
Won't this break some user SQLs?
Thanks!
Best regards, Andrey Borodin.