From 62f3f4cb2121abd91c9d91d99e2bd9b973bd72b1 Mon Sep 17 00:00:00 2001
From: Jim Jones <jim.jones@uni-muenster.de>
Date: Thu, 1 Oct 2026 23:04:24 +0200
Subject: [PATCH v5] Add table comments in CREATE TABLE LIKE INCLUDING COMMENTS
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

CREATE TABLE ... (LIKE ... INCLUDING COMMENTS), and therefore INCLUDING
ALL, copies the comments on the source relation's columns, constraints,
indexes, and extended statistics, but not the comment on the source
relation itself.  Copy that one too, so that INCLUDING COMMENTS covers
every comment attached to the objects that LIKE copies.

The table comment is copied only when the command contains a single
LIKE clause.  A table built from several source relations is not
described by any one of their comments, and there is no obvious way to
choose between or combine them, so in that case no table comment is
copied.  Comments on the copied columns, constraints, indexes, and
statistics are unaffected by this.

Author: Jim Jones <jim.jones@uni-muenster.de>
Reviewed-by: Alex Liapychev <coder.sam@gmail.com>
Reviewed-by: Eddie Cho <chochoforwork@gmail.com>
Reviewed-by: Tomas Vondra <tomas@vondra.me>
Reviewed-by: ZizhuanLiu X-MAN <44973863@qq.com>
Reviewed-by: Carlos Alves <carlos@alves.inf.br>
Reviewed-by: Hüseyin Demir <huseyin.d3r@gmail.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: David. G. Johnston <david.g.johnston@gmail.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Matheus Alcantara <matheusssilv97@gmail.com>
Reviewed-by: Fujii Masao <masao.fujii@gmail.com>
Discussion: https://www.postgresql.org/message-id/flat/e08cb97f-0364-4002-9cda-3c16b42e4136%40uni-muenster.de
---
 doc/src/sgml/ref/create_table.sgml            |  17 ++-
 src/backend/parser/parse_utilcmd.c            |  48 ++++++++
 .../regress/expected/create_table_like.out    | 109 ++++++++++++++++++
 src/test/regress/sql/create_table_like.sql    |  70 +++++++++++
 4 files changed, 239 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index fef24d8f3a2..4e36b121150 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>
+          The comment on the source relation is copied only if the command
+          contains a single <literal>LIKE</literal> clause.  If there are
+          several, no source relation's comment is copied to the new table,
+          though the comments on their columns, constraints, indexes, and
+          extended statistics still are.
          </para>
         </listitem>
        </varlistentry>
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f0c7755b8fb..92d05956a92 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -86,6 +86,8 @@ typedef struct
 	List	   *fkconstraints;	/* FOREIGN KEY constraints */
 	List	   *ixconstraints;	/* index-creating constraints */
 	List	   *likeclauses;	/* LIKE clauses that need post-processing */
+	char	   *tablecomment;	/* comment on the LIKE source relation */
+	int			nlikeclauses;	/* # of LIKE clauses seen so far */
 	List	   *blist;			/* "before list" of things to do before
 								 * creating the table */
 	List	   *alist;			/* "after list" of things to do after creating
@@ -251,6 +253,8 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 	cxt.fkconstraints = NIL;
 	cxt.ixconstraints = NIL;
 	cxt.likeclauses = NIL;
+	cxt.tablecomment = NULL;
+	cxt.nlikeclauses = 0;
 	cxt.blist = NIL;
 	cxt.alist = NIL;
 	cxt.pkey = NULL;
@@ -300,6 +304,27 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 		}
 	}
 
+	/*
+	 * Copy the comment on the LIKE source relation to the new table, but only
+	 * if there is exactly one LIKE clause.  A table built from several source
+	 * relations is not described by any one of their comments, and there is
+	 * no obvious way to combine them, so we copy none.
+	 */
+	if (cxt.nlikeclauses == 1 && 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;
+
+		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
@@ -1311,6 +1336,27 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		}
 	}
 
+	/*
+	 * Remember the comment on the source relation itself, if requested.
+	 * transformCreateStmt decides whether to copy it, since that depends on
+	 * how many LIKE clauses there are.  There's no need to look up the
+	 * comment for any clause after the first.
+	 */
+	cxt->nlikeclauses++;
+	if (cxt->nlikeclauses == 1 &&
+		(table_like_clause->options & CREATE_TABLE_LIKE_COMMENTS))
+	{
+		/* A composite type's comment is attached to its pg_type entry */
+		if (relation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE)
+			cxt->tablecomment = GetComment(relation->rd_rel->reltype,
+										   TypeRelationId,
+										   0);
+		else
+			cxt->tablecomment = GetComment(RelationGetRelid(relation),
+										   RelationRelationId,
+										   0);
+	}
+
 	/*
 	 * We cannot yet deal with defaults, CHECK constraints, indexes, or
 	 * statistics, since we don't yet know what column numbers the copied
@@ -3624,6 +3670,8 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 	cxt.fkconstraints = NIL;
 	cxt.ixconstraints = NIL;
 	cxt.likeclauses = NIL;
+	cxt.tablecomment = NULL;
+	cxt.nlikeclauses = 0;
 	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..8a491f338bc 100644
--- a/src/test/regress/expected/create_table_like.out
+++ b/src/test/regress/expected/create_table_like.out
@@ -698,6 +698,115 @@ SELECT attname, attcompression FROM pg_attribute
  e       | 
 (5 rows)
 
+-- LIKE ... INCLUDING COMMENTS
+-- Test copying the comment on the source relation itself
+CREATE TABLE ctlt_comment1 (a int);
+COMMENT ON TABLE ctlt_comment1 IS 'comment1';
+COMMENT ON COLUMN ctlt_comment1.a IS 'column a';
+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)
+
+-- With several LIKE clauses no table comment is copied, even if only one of
+-- them specifies INCLUDING COMMENTS
+CREATE TABLE ctlt_one_of_many (
+    LIKE ctlft_comment2,
+    LIKE ctlv_comment3 INCLUDING COMMENTS,
+    LIKE ctltt_comment4 INCLUDING ALL EXCLUDING COMMENTS
+);
+SELECT obj_description('ctlt_one_of_many'::regclass, 'pg_class') IS NULL AS no_comment;
+ no_comment 
+------------
+ t
+(1 row)
+
+-- Likewise if several of them specify INCLUDING COMMENTS (INCLUDING ALL implies
+-- it); column comments are still copied
+CREATE TABLE ctlt_multi_comments (
+    LIKE ctlt_comment1 INCLUDING ALL,
+    LIKE ctlv_comment3 INCLUDING COMMENTS
+);
+SELECT obj_description('ctlt_multi_comments'::regclass, 'pg_class') IS NULL AS no_comment;
+ no_comment 
+------------
+ t
+(1 row)
+
+SELECT col_description('ctlt_multi_comments'::regclass, 1) AS column_comment;
+ column_comment 
+----------------
+ column a
+(1 row)
+
+-- Likewise if only one of the sources actually has a comment
+CREATE TABLE ctlt_nocomment (e int);
+CREATE TABLE ctlt_gap_comments (
+    LIKE ctlt_nocomment INCLUDING COMMENTS,
+    LIKE ctlt_comment1 INCLUDING COMMENTS
+);
+SELECT obj_description('ctlt_gap_comments'::regclass, 'pg_class') IS NULL AS no_comment;
+ no_comment 
+------------
+ t
+(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_one_of_many, 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..1023e8c5a5c 100644
--- a/src/test/regress/sql/create_table_like.sql
+++ b/src/test/regress/sql/create_table_like.sql
@@ -276,6 +276,76 @@ 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 copying the comment on the source relation itself
+CREATE TABLE ctlt_comment1 (a int);
+COMMENT ON TABLE ctlt_comment1 IS 'comment1';
+COMMENT ON COLUMN ctlt_comment1.a IS 'column a';
+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;
+
+-- With several LIKE clauses no table comment is copied, even if only one of
+-- them specifies INCLUDING COMMENTS
+CREATE TABLE ctlt_one_of_many (
+    LIKE ctlft_comment2,
+    LIKE ctlv_comment3 INCLUDING COMMENTS,
+    LIKE ctltt_comment4 INCLUDING ALL EXCLUDING COMMENTS
+);
+SELECT obj_description('ctlt_one_of_many'::regclass, 'pg_class') IS NULL AS no_comment;
+
+-- Likewise if several of them specify INCLUDING COMMENTS (INCLUDING ALL implies
+-- it); column comments are still copied
+CREATE TABLE ctlt_multi_comments (
+    LIKE ctlt_comment1 INCLUDING ALL,
+    LIKE ctlv_comment3 INCLUDING COMMENTS
+);
+SELECT obj_description('ctlt_multi_comments'::regclass, 'pg_class') IS NULL AS no_comment;
+SELECT col_description('ctlt_multi_comments'::regclass, 1) AS column_comment;
+
+-- Likewise if only one of the sources actually has a comment
+CREATE TABLE ctlt_nocomment (e int);
+CREATE TABLE ctlt_gap_comments (
+    LIKE ctlt_nocomment INCLUDING COMMENTS,
+    LIKE ctlt_comment1 INCLUDING COMMENTS
+);
+SELECT obj_description('ctlt_gap_comments'::regclass, 'pg_class') IS NULL AS no_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_one_of_many, 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

