From 0edc783e6ed1e50a54d005f280e2e372bd6fab35 Mon Sep 17 00:00:00 2001
From: Jim Jones <jim.jones@uni-muenster.de>
Date: Tue, 22 Sep 2026 21:18:52 +0200
Subject: [PATCH v4] Add table comments in CREATE TABLE LIKE INCLUDING COMMENTS

When using CREATE TABLE ... LIKE ... INCLUDING COMMENTS (or INCLUDING
ALL), table-level comments were not being copied to the new table, even
though column comments, constraint comments, index comments, and
statistics comments were properly copied. This patch extends the
feature to also copy the table's own comment to the target table.

When multiple LIKE clauses specify INCLUDING COMMENTS and the source
tables have table-level comments, the comments are now concatenated in
the target table, separated by newlines, in the order that the LIKE
clauses appear. This allows users to preserve comment information from
all source tables when creating tables that combine properties from
multiple sources.
---
 doc/src/sgml/ref/create_table.sgml            | 17 +++-
 src/backend/parser/parse_utilcmd.c            | 70 ++++++++++++++
 .../regress/expected/create_table_like.out    | 96 +++++++++++++++++++
 src/test/regress/sql/create_table_like.sql    | 63 ++++++++++++
 4 files changed, 241 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index fef24d8f3a2..aff7ae4dc84 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -692,11 +692,18 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
         <term><literal>INCLUDING COMMENTS</literal></term>
         <listitem>
          <para>
-          Comments for the copied columns, check constraints,
-          not-null constraints, indexes, and extended statistics will be
-          copied.  The default behavior is to exclude comments, resulting in
-          the corresponding objects in the new table having no
-          comments.
+          Comments for the copied columns, check constraints, not-null
+          constraints, indexes, and extended statistics will be copied, as
+          will the comment on the source relation itself.  The default
+          behavior is to exclude comments, resulting in the corresponding
+          objects in the new table having no comments.
+         </para>
+         <para>
+          If multiple <literal>LIKE</literal> clauses specify
+          <literal>INCLUDING COMMENTS</literal> and the source relations have
+          table-level comments, these comments will be concatenated in the new
+          table, separated by newlines, in the order that the
+          <literal>LIKE</literal> clauses appear.
          </para>
         </listitem>
        </varlistentry>
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f838311090b..10b35d80407 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -45,6 +45,7 @@
 #include "commands/sequence.h"
 #include "commands/tablecmds.h"
 #include "commands/tablespace.h"
+#include "lib/stringinfo.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -63,6 +64,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
+#include "utils/memutils.h"
 #include "utils/partcache.h"
 #include "utils/rel.h"
 #include "utils/ruleutils.h"
@@ -86,6 +88,7 @@ typedef struct
 	List	   *fkconstraints;	/* FOREIGN KEY constraints */
 	List	   *ixconstraints;	/* index-creating constraints */
 	List	   *likeclauses;	/* LIKE clauses that need post-processing */
+	StringInfo	tablecomment;	/* comment collected from LIKE clauses */
 	List	   *blist;			/* "before list" of things to do before
 								 * creating the table */
 	List	   *alist;			/* "after list" of things to do after creating
@@ -251,6 +254,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 	cxt.fkconstraints = NIL;
 	cxt.ixconstraints = NIL;
 	cxt.likeclauses = NIL;
+	cxt.tablecomment = NULL;
 	cxt.blist = NIL;
 	cxt.alist = NIL;
 	cxt.pkey = NULL;
@@ -300,6 +304,25 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 		}
 	}
 
+	/*
+	 * If any LIKE clause collected a comment on its source table, emit a
+	 * single command applying the accumulated text to the new table.
+	 */
+	if (cxt.tablecomment != NULL)
+	{
+		CommentStmt *cstmt = makeNode(CommentStmt);
+
+		cstmt->objtype = cxt.isforeign ? OBJECT_FOREIGN_TABLE : OBJECT_TABLE;
+		if (cxt.relation->schemaname)
+			cstmt->object = (Node *) list_make2(makeString(cxt.relation->schemaname),
+												makeString(cxt.relation->relname));
+		else
+			cstmt->object = (Node *) list_make1(makeString(cxt.relation->relname));
+		cstmt->comment = cxt.tablecomment->data;
+
+		cxt.alist = lappend(cxt.alist, cstmt);
+	}
+
 	/*
 	 * Transfer anything we already have in cxt.alist into save_alist, to keep
 	 * it separate from the output of transformIndexConstraints.  (This may
@@ -1309,6 +1332,52 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		}
 	}
 
+	/*
+	 * Copy the comment on the source relation itself, if requested.  Several
+	 * LIKE clauses may each supply one; accumulate them in clause order and
+	 * let transformCreateStmt emit a single COMMENT command for the result.
+	 */
+	if (table_like_clause->options & CREATE_TABLE_LIKE_COMMENTS)
+	{
+		char	   *tblcomment;
+
+		/* A composite type's comment is attached to its pg_type entry */
+		if (relation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE)
+			tblcomment = GetComment(relation->rd_rel->reltype,
+									TypeRelationId,
+									0);
+		else
+			tblcomment = GetComment(RelationGetRelid(relation),
+									RelationRelationId,
+									0);
+
+		if (tblcomment != NULL)
+		{
+			size_t		needed = strlen(tblcomment);
+
+			if (cxt->tablecomment == NULL)
+				cxt->tablecomment = makeStringInfo();
+			else
+				needed++;		/* the newline separator */
+
+			/*
+			 * Reject an over-long result explicitly, rather than letting the
+			 * user hit enlargeStringInfo()'s much less informative complaint.
+			 */
+			if ((size_t) cxt->tablecomment->len + needed >= MaxAllocSize)
+				ereport(ERROR,
+						(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+						 errmsg("comment for relation \"%s\" is too long",
+								cxt->relation->relname),
+						 errdetail("The comment is the concatenation of comments copied from multiple relations named in LIKE clauses, and the result exceeds the maximum size allowed for a comment.")));
+
+			if (cxt->tablecomment->len > 0)
+				appendStringInfoChar(cxt->tablecomment, '\n');
+			appendStringInfoString(cxt->tablecomment, tblcomment);
+			pfree(tblcomment);
+		}
+	}
+
 	/*
 	 * We cannot yet deal with defaults, CHECK constraints, indexes, or
 	 * statistics, since we don't yet know what column numbers the copied
@@ -3622,6 +3691,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 	cxt.fkconstraints = NIL;
 	cxt.ixconstraints = NIL;
 	cxt.likeclauses = NIL;
+	cxt.tablecomment = NULL;
 	cxt.blist = NIL;
 	cxt.alist = NIL;
 	cxt.pkey = NULL;
diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out
index a23735b5fb4..58db2bab784 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -698,6 +698,102 @@ SELECT attname, attcompression FROM pg_attribute
  e       | 
 (5 rows)
 
+-- LIKE ... INCLUDING COMMENTS
+-- Test multiple LIKE clauses with table comments
+CREATE TABLE ctlt_comment1 (a int);
+COMMENT ON TABLE ctlt_comment1 IS 'comment1';
+CREATE FOREIGN TABLE ctlft_comment2 (b int) SERVER ctl_s0;
+COMMENT ON FOREIGN TABLE ctlft_comment2 IS 'comment2';
+CREATE VIEW ctlv_comment3 AS SELECT 42 AS c;
+COMMENT ON VIEW ctlv_comment3 IS 'comment3';
+CREATE TEMPORARY TABLE ctltt_comment4 (d int);
+COMMENT ON TABLE ctltt_comment4 IS 'comment4';
+-- Single LIKE clause should copy table comment when INCLUDING COMMENTS is specified.
+CREATE TABLE ctlt_single_comment (LIKE ctlt_comment1 INCLUDING COMMENTS);
+SELECT obj_description('ctlt_single_comment'::regclass, 'pg_class') AS table_comment;
+ table_comment 
+---------------
+ comment1
+(1 row)
+
+-- Single LIKE clause should copy table comment when INCLUDING ALL is specified.
+CREATE TABLE ctlt_single_comment_all (LIKE ctlt_comment1 INCLUDING ALL);
+SELECT obj_description('ctlt_single_comment_all'::regclass, 'pg_class') AS table_comment;
+ table_comment 
+---------------
+ comment1
+(1 row)
+
+-- Multiple LIKE clauses should concatenate table comments when INCLUDING COMMENTS is specified.
+-- INCLUDING ALL implies INCLUDING COMMENTS, and no comment is copied when neither is specified.
+-- The order of comments should be the same as the order of LIKE clauses
+CREATE TABLE ctlt_multi_comments (
+    LIKE ctlt_comment1 INCLUDING ALL,
+    LIKE ctlv_comment3 INCLUDING COMMENTS,
+    LIKE ctlft_comment2,
+    LIKE ctltt_comment4 INCLUDING COMMENTS
+);
+SELECT obj_description('ctlt_multi_comments'::regclass, 'pg_class') AS table_comment;
+ table_comment 
+---------------
+ comment1     +
+ comment3     +
+ comment4
+(1 row)
+
+-- a comment-less source in the middle must not leave a stray separator
+CREATE TABLE ctlt_nocomment (e int);
+CREATE TABLE ctlt_gap_comments (
+    LIKE ctlt_comment1 INCLUDING COMMENTS,
+    LIKE ctlt_nocomment INCLUDING COMMENTS,
+    LIKE ctltt_comment4 INCLUDING COMMENTS
+);
+SELECT obj_description('ctlt_gap_comments'::regclass, 'pg_class') AS table_comment;
+ table_comment 
+---------------
+ comment1     +
+ comment4
+(1 row)
+
+-- Test that INCLUDING COMMENTS works for target foreign tables
+CREATE FOREIGN TABLE ctlft_comment4 (LIKE ctlt_comment1 INCLUDING COMMENTS) SERVER ctl_s0;
+SELECT obj_description('ctlft_comment4'::regclass, 'pg_class') AS table_comment;
+ table_comment 
+---------------
+ comment1
+(1 row)
+
+-- Test that INCLUDING COMMENTS works for target temporary tables
+CREATE TEMPORARY TABLE ctltt_comment5 (LIKE ctlt_comment1 INCLUDING COMMENTS);
+SELECT obj_description('ctltt_comment5'::regclass, 'pg_class') AS table_comment;
+ table_comment 
+---------------
+ comment1
+(1 row)
+
+-- INCLUDING ALL EXCLUDING COMMENTS must not copy the comment
+CREATE TABLE ctlt_no_comment (LIKE ctlt_comment1 INCLUDING ALL EXCLUDING COMMENTS);
+SELECT obj_description('ctlt_no_comment'::regclass, 'pg_class') IS NULL AS no_comment;
+ no_comment 
+------------
+ t
+(1 row)
+
+-- A composite type's comment is stored on its pg_type entry, not on pg_class,
+-- but it should be copied just the same
+CREATE TYPE ctlty_comment6 AS (f int);
+COMMENT ON TYPE ctlty_comment6 IS 'comment6';
+CREATE TABLE ctlt_type_comment (LIKE ctlty_comment6 INCLUDING COMMENTS);
+SELECT obj_description('ctlt_type_comment'::regclass, 'pg_class') AS table_comment;
+ table_comment 
+---------------
+ comment6
+(1 row)
+
+DROP TABLE ctlt_comment1, ctlt_multi_comments, ctltt_comment4, ctltt_comment5, ctlt_single_comment, ctlt_nocomment, ctlt_gap_comments, ctlt_no_comment, ctlt_single_comment_all, ctlt_type_comment;
+DROP FOREIGN TABLE ctlft_comment2, ctlft_comment4;
+DROP VIEW ctlv_comment3;
+DROP TYPE ctlty_comment6;
 -- LIKE ... INCLUDING STATISTICS with dropped columns in the parent,
 -- so stxkeys attnums are not contiguous.
 CREATE TABLE ctl_stats3_parent (a int, b int, c int);
diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql
index d52a93ef131..d2b9c634d53 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -276,6 +276,69 @@ CREATE FOREIGN TABLE ctl_foreign_table2(LIKE ctl_table INCLUDING ALL) SERVER ctl
 SELECT attname, attcompression FROM pg_attribute
   WHERE attrelid = 'ctl_foreign_table2'::regclass and attnum > 0 ORDER BY attnum;
 
+-- LIKE ... INCLUDING COMMENTS
+-- Test multiple LIKE clauses with table comments
+CREATE TABLE ctlt_comment1 (a int);
+COMMENT ON TABLE ctlt_comment1 IS 'comment1';
+CREATE FOREIGN TABLE ctlft_comment2 (b int) SERVER ctl_s0;
+COMMENT ON FOREIGN TABLE ctlft_comment2 IS 'comment2';
+CREATE VIEW ctlv_comment3 AS SELECT 42 AS c;
+COMMENT ON VIEW ctlv_comment3 IS 'comment3';
+CREATE TEMPORARY TABLE ctltt_comment4 (d int);
+COMMENT ON TABLE ctltt_comment4 IS 'comment4';
+
+-- Single LIKE clause should copy table comment when INCLUDING COMMENTS is specified.
+CREATE TABLE ctlt_single_comment (LIKE ctlt_comment1 INCLUDING COMMENTS);
+SELECT obj_description('ctlt_single_comment'::regclass, 'pg_class') AS table_comment;
+
+-- Single LIKE clause should copy table comment when INCLUDING ALL is specified.
+CREATE TABLE ctlt_single_comment_all (LIKE ctlt_comment1 INCLUDING ALL);
+SELECT obj_description('ctlt_single_comment_all'::regclass, 'pg_class') AS table_comment;
+
+-- Multiple LIKE clauses should concatenate table comments when INCLUDING COMMENTS is specified.
+-- INCLUDING ALL implies INCLUDING COMMENTS, and no comment is copied when neither is specified.
+-- The order of comments should be the same as the order of LIKE clauses
+CREATE TABLE ctlt_multi_comments (
+    LIKE ctlt_comment1 INCLUDING ALL,
+    LIKE ctlv_comment3 INCLUDING COMMENTS,
+    LIKE ctlft_comment2,
+    LIKE ctltt_comment4 INCLUDING COMMENTS
+);
+SELECT obj_description('ctlt_multi_comments'::regclass, 'pg_class') AS table_comment;
+
+-- a comment-less source in the middle must not leave a stray separator
+CREATE TABLE ctlt_nocomment (e int);
+CREATE TABLE ctlt_gap_comments (
+    LIKE ctlt_comment1 INCLUDING COMMENTS,
+    LIKE ctlt_nocomment INCLUDING COMMENTS,
+    LIKE ctltt_comment4 INCLUDING COMMENTS
+);
+SELECT obj_description('ctlt_gap_comments'::regclass, 'pg_class') AS table_comment;
+
+-- Test that INCLUDING COMMENTS works for target foreign tables
+CREATE FOREIGN TABLE ctlft_comment4 (LIKE ctlt_comment1 INCLUDING COMMENTS) SERVER ctl_s0;
+SELECT obj_description('ctlft_comment4'::regclass, 'pg_class') AS table_comment;
+
+-- Test that INCLUDING COMMENTS works for target temporary tables
+CREATE TEMPORARY TABLE ctltt_comment5 (LIKE ctlt_comment1 INCLUDING COMMENTS);
+SELECT obj_description('ctltt_comment5'::regclass, 'pg_class') AS table_comment;
+
+-- INCLUDING ALL EXCLUDING COMMENTS must not copy the comment
+CREATE TABLE ctlt_no_comment (LIKE ctlt_comment1 INCLUDING ALL EXCLUDING COMMENTS);
+SELECT obj_description('ctlt_no_comment'::regclass, 'pg_class') IS NULL AS no_comment;
+
+-- A composite type's comment is stored on its pg_type entry, not on pg_class,
+-- but it should be copied just the same
+CREATE TYPE ctlty_comment6 AS (f int);
+COMMENT ON TYPE ctlty_comment6 IS 'comment6';
+CREATE TABLE ctlt_type_comment (LIKE ctlty_comment6 INCLUDING COMMENTS);
+SELECT obj_description('ctlt_type_comment'::regclass, 'pg_class') AS table_comment;
+
+DROP TABLE ctlt_comment1, ctlt_multi_comments, ctltt_comment4, ctltt_comment5, ctlt_single_comment, ctlt_nocomment, ctlt_gap_comments, ctlt_no_comment, ctlt_single_comment_all, ctlt_type_comment;
+DROP FOREIGN TABLE ctlft_comment2, ctlft_comment4;
+DROP VIEW ctlv_comment3;
+DROP TYPE ctlty_comment6;
+
 -- LIKE ... INCLUDING STATISTICS with dropped columns in the parent,
 -- so stxkeys attnums are not contiguous.
 CREATE TABLE ctl_stats3_parent (a int, b int, c int);
-- 
2.55.0

