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

