hi.

demo:

 CREATE TABLE coll_t (c) AS VALUES ('bar'::text);
 CREATE POLICY coll_p ON coll_t USING (c < ('foo'::text COLLATE "C"));
+-- coll_t1 row security is not enabled, but we still copy it's policies
+CREATE TABLE coll_t1(LIKE coll_t INCLUDING POLICIES);
+\d coll_t1
+       Table "regress_rls_schema.coll_t1"
+ Column | Type | Collation | Nullable | Default
+--------+------+-----------+----------+---------
+ c      | text |           |          |
+Policies (row security disabled):
+    POLICY "coll_p"
+      USING ((c < ('foo'::text COLLATE "C")))

by default, new table row security will be disabled, policy object comments are
copied if INCLUDING COMMENTS is specified.

ALTER TABLE SET DATA TYPE, changing a column type typically pulls the object and
reconstructs the corresponding CREATE or ALTER command string.
however CREATE TABLE LIKE retrieves the object’s catalog data and adjusts Vars
to build a Create.*Stmt node.

Here, we generate a CreatePolicyStmt from the source relation’s pg_policy
metadata and then pass it to CreatePolicy.

CreatePolicy normally performs parse analysis on CreatePolicyStmt->qual and
CreatePolicyStmt->with_check.  However, since the pg_policy entries from the
source relation already have their qual and check_qual parse analyzed, we cannot
re-analyze them.  Similar to transformStatsStmt, we therefore need a
transformPolicyStmt.


v1-0001: refactor CreatePolicy, add function transformPolicyStmt
briefly explained in [1].
v1-0002: CREATE TABLE LIKE INCLUDING-POLICIES

[1] 
https://postgr.es/m/CACJufxGPcBzdL9T6Qh=OFecN8zqxuU0QXfYF8F3WYV=uzwy...@mail.gmail.com
From d94917c777c3fd454a438b31806e35ffe74ac5f1 Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
Date: Mon, 15 Sep 2025 11:58:56 +0800
Subject: [PATCH v1 2/2] CREATE TABLE LIKE(INCLUDING POLICIES)

We already acquire an AccessShareLock in transformTableLikeClause for the source table.
CREATE/ALTER/DROP POLICY requires an AccessExclusiveLock on the table, there's
no chance that a security policy will be modified while executing CREATE TABLE
LIKE.

discussion: https://postgr.es/m/
---
 doc/src/sgml/ref/create_table.sgml        |  18 +-
 src/backend/commands/policy.c             |  24 ++-
 src/backend/parser/gram.y                 |   7 +-
 src/backend/parser/parse_utilcmd.c        | 194 +++++++++++++++++++++-
 src/include/nodes/parsenodes.h            |   9 +
 src/include/parser/kwlist.h               |   1 +
 src/test/regress/expected/rowsecurity.out | 105 +++++++++++-
 src/test/regress/sql/rowsecurity.sql      |  43 +++++
 8 files changed, 391 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index dc000e913c1..5f402231dfa 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 | POLICIES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -672,9 +672,9 @@ 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, policies, and indexes will be
           copied.  The default behavior is to exclude comments, resulting in
-          the copied columns and constraints in the new table having no
+          the copied columns, policies, and constraints in the new table having no
           comments.
          </para>
         </listitem>
@@ -753,6 +753,18 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-policies">
+        <term><literal>INCLUDING POLICIES</literal></term>
+        <listitem>
+         <para>
+          Any row-level security policies are copied to the new table.
+          Note that row-level security is not enabled to the new table,
+          using <command>ALTER TABLE ... ENABLE ROW LEVEL SECURITY</command>
+          in order for created policies to be applied to the new table.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-statistics">
         <term><literal>INCLUDING STATISTICS</literal></term>
         <listitem>
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 799e1e3968a..08a80de1a0f 100644
--- a/src/backend/commands/policy.c
+++ b/src/backend/commands/policy.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_policy.h"
 #include "catalog/pg_type.h"
+#include "commands/comment.h"
 #include "commands/policy.h"
 #include "miscadmin.h"
 #include "nodes/pg_list.h"
@@ -612,8 +613,22 @@ CreatePolicy(CreatePolicyStmt *stmt, const char *queryString)
 				 errmsg("only WITH CHECK expression allowed for INSERT")));
 
 	/* Collect role ids */
-	role_oids = policy_role_list_to_array(stmt->roles, &nitems);
-	role_ids = construct_array_builtin(role_oids, nitems, OIDOID);
+	if (stmt->roles != NIL)
+	{
+		role_oids = policy_role_list_to_array(stmt->roles, &nitems);
+		role_ids = construct_array_builtin(role_oids, nitems, OIDOID);
+	}
+	else
+	{
+		Assert(stmt->rolesId != NIL);
+		nitems = list_length(stmt->rolesId);
+
+		role_oids = (Datum *) palloc(nitems * sizeof(Datum));
+		foreach_oid(roleoid, stmt->rolesId)
+			role_oids[foreach_current_index(roleoid)] =  ObjectIdGetDatum(roleoid);
+
+		role_ids = construct_array_builtin(role_oids, nitems, OIDOID);
+	}
 
 	/* zero-clear */
 	memset(values, 0, sizeof(values));
@@ -738,6 +753,11 @@ CreatePolicy(CreatePolicyStmt *stmt, const char *queryString)
 	relation_close(target_table, NoLock);
 	table_close(pg_policy_rel, RowExclusiveLock);
 
+	/* Add any requested comment */
+	if (stmt->policycomment != NULL)
+		CreateComments(policy_id, PolicyRelationId, 0,
+					   stmt->policycomment);
+
 	return myself;
 }
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 8016c58b49c..210f1f0ee8e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -756,7 +756,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
 	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH
-	PERIOD PLACING PLAN PLANS POLICY
+	PERIOD PLACING PLAN PLANS POLICIES POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
@@ -4214,6 +4214,7 @@ TableLikeOption:
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
+				| POLICIES			{ $$ = CREATE_TABLE_LIKE_POLICIES; }
 				| STATISTICS		{ $$ = CREATE_TABLE_LIKE_STATISTICS; }
 				| STORAGE			{ $$ = CREATE_TABLE_LIKE_STORAGE; }
 				| ALL				{ $$ = CREATE_TABLE_LIKE_ALL; }
@@ -5949,6 +5950,8 @@ CreatePolicyStmt:
 					n->qual = $9;
 					n->with_check = $10;
 					n->transformed = false;
+					n->policycomment = NULL;
+					n->rolesId = NIL;
 					$$ = (Node *) n;
 				}
 		;
@@ -17937,6 +17940,7 @@ unreserved_keyword:
 			| PERIOD
 			| PLAN
 			| PLANS
+			| POLICIES
 			| POLICY
 			| PRECEDING
 			| PREPARE
@@ -18565,6 +18569,7 @@ bare_label_keyword:
 			| PLACING
 			| PLAN
 			| PLANS
+			| POLICIES
 			| POLICY
 			| POSITION
 			| PRECEDING
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 394a037e817..3f552d6be85 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_policy.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_type.h"
 #include "commands/comment.h"
@@ -61,6 +62,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"
@@ -120,6 +122,11 @@ static CreateStatsStmt *generateClonedExtStatsStmt(RangeVar *heapRel,
 												   Oid heapRelid,
 												   Oid source_statsid,
 												   const AttrMap *attmap);
+static CreatePolicyStmt *generateClonedPolicyStmt(RangeVar *heapRel,
+												  Relation parent_rel,
+												  Relation pg_policy,
+												  HeapTuple poltup,
+												  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);
@@ -1304,8 +1311,8 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 	}
 
 	/*
-	 * We cannot yet deal with defaults, CHECK constraints, indexes, or
-	 * statistics, since we don't yet know what column numbers the copied
+	 * We cannot yet deal with defaults, CHECK constraints, indexes, statistics
+	 * or policies, since we don't yet know what column numbers the copied
 	 * columns will have in the finished table.  If any of those options are
 	 * specified, add the LIKE clause to cxt->likeclauses so that
 	 * expandTableLikeClause will be called after we do know that.
@@ -1318,7 +1325,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_POLICIES))
 	{
 		table_like_clause->relationOid = RelationGetRelid(relation);
 		cxt->likeclauses = lappend(cxt->likeclauses, table_like_clause);
@@ -1332,6 +1340,26 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 	table_close(relation, NoLock);
 }
 
+static char *
+get_policy_applied_command(char polcmd)
+{
+	if (polcmd == '*')
+		return pstrdup("all");
+	else if (polcmd == ACL_SELECT_CHR)
+		return pstrdup("select");
+	else if (polcmd == ACL_INSERT_CHR)
+		return pstrdup("insert");
+	else if (polcmd == ACL_UPDATE_CHR)
+		return pstrdup("update");
+	else if (polcmd == ACL_DELETE_CHR)
+		return pstrdup("delete");
+	else
+	{
+		elog(ERROR, "unrecognized policy command");
+		return NULL;
+	}
+}
+
 /*
  * expandTableLikeClause
  *
@@ -1622,6 +1650,56 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause)
 		list_free(parent_extstats);
 	}
 
+	/*
+	 * Process table row level security policies if required.
+	 */
+	if (table_like_clause->options & CREATE_TABLE_LIKE_POLICIES)
+	{
+		Relation	catalog;
+		ScanKeyData skey;
+		SysScanDesc sscan;
+		HeapTuple	tuple;
+
+		/*
+		 * Scan pg_policy for any RLS policies defined on source relation.  The
+		 * order of visiting the policies does not matter, since we are copying
+		 * all of them to the new relation.
+		*/
+		catalog = table_open(PolicyRelationId, AccessShareLock);
+		ScanKeyInit(&skey,
+					Anum_pg_policy_polrelid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(RelationGetRelid(relation)));
+		sscan = systable_beginscan(catalog, PolicyPolrelidPolnameIndexId, true,
+								   NULL, 1, &skey);
+
+		while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+		{
+			CreatePolicyStmt *polstmt;
+
+			polstmt = generateClonedPolicyStmt(heapRel, relation, catalog, tuple, attmap);
+
+			/* Copy comment on policies object, if requested */
+			if (table_like_clause->options & CREATE_TABLE_LIKE_COMMENTS)
+			{
+				Form_pg_policy policy_form;
+				policy_form = (Form_pg_policy) GETSTRUCT(tuple);
+
+				comment = GetComment(policy_form->oid, PolicyRelationId, 0);
+
+				/*
+				 * We make use of CreatePolicyStmt's policycomment option, so as
+				 * not to need to know now what name the policies will have.
+				*/
+				polstmt->policycomment = comment;
+			}
+			result = lappend(result, polstmt);
+		}
+
+		systable_endscan(sscan);
+		table_close(catalog, AccessShareLock);
+	}
+
 	/* Done with child rel */
 	table_close(childrel, NoLock);
 
@@ -2163,6 +2241,116 @@ generateClonedExtStatsStmt(RangeVar *heapRel, Oid heapRelid,
 	return stats;
 }
 
+/*
+ * Generate a CreatePolicyStmt node using information from an already existing
+ * pg_policy tuple "poltup", which is owned by parent_rel.
+ *
+ * Attribute numbers in expression Vars are adjusted according to attmap.
+ */
+static CreatePolicyStmt *
+generateClonedPolicyStmt(RangeVar *heapRel, Relation parent_rel, Relation pg_policy,
+						 HeapTuple poltup, const AttrMap *attmap)
+{
+	Datum		datum;
+	bool		isnull;
+	char	   *str_value;
+	Oid		   *rawarr;
+	ArrayType  *arr;
+	int			numkeys;
+	bool		found_whole_row;
+	CreatePolicyStmt *polstmt;
+	Form_pg_policy policy_form;
+
+	policy_form = (Form_pg_policy) GETSTRUCT(poltup);
+
+	polstmt = makeNode(CreatePolicyStmt);
+	polstmt->policy_name = pstrdup(NameStr(policy_form->polname));
+	polstmt->table = copyObject(heapRel);
+	polstmt->cmd_name = get_policy_applied_command(policy_form->polcmd);
+	polstmt->permissive = policy_form->polpermissive;
+	polstmt->roles = NIL;
+	polstmt->rolesId = NIL;
+
+	/* Get policy roles */
+	datum = heap_getattr(poltup, Anum_pg_policy_polroles,
+						 RelationGetDescr(pg_policy), &isnull);
+	/* shouldn't be null, but let's check for luck */
+	if (isnull)
+		elog(ERROR, "unexpected null value in pg_policy.polroles");
+
+	arr = DatumGetArrayTypeP(datum);
+	if (ARR_NDIM(arr) != 1 ||
+		ARR_HASNULL(arr) ||
+		ARR_ELEMTYPE(arr) != OIDOID)
+		elog(ERROR, "policy roles is not a 1-D Oid array");
+	rawarr = (Oid *) ARR_DATA_PTR(arr);
+	numkeys = ARR_DIMS(arr)[0];
+
+	/* stash a List of the role Oids in our CreatePolicyStmt node */
+	for (int i = 0; i < numkeys; i++)
+		polstmt->rolesId = lappend_oid(polstmt->rolesId, rawarr[i]);
+
+	/* Get policy qual */
+	datum = heap_getattr(poltup, Anum_pg_policy_polqual,
+						 RelationGetDescr(pg_policy), &isnull);
+	if (!isnull)
+	{
+		str_value = TextDatumGetCString(datum);
+		polstmt->qual = stringToNode(str_value);
+
+		/* Adjust Vars to match new table's column numbering */
+		polstmt->qual = map_variable_attnos(polstmt->qual,
+											1, 0,
+											attmap,
+											InvalidOid,
+											&found_whole_row);
+		/*
+		 * Prevent this for the same reason as for constraints above.
+		 */
+		if (found_whole_row)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot convert whole-row table reference"),
+					errdetail("Security policy \"%s\" contains a whole-row reference to table \"%s\".",
+								NameStr(policy_form->polname),
+								RelationGetRelationName(parent_rel)));
+		pfree(str_value);
+	}
+
+	/* Get WITH CHECK qual */
+	datum = heap_getattr(poltup, Anum_pg_policy_polwithcheck,
+						 RelationGetDescr(pg_policy), &isnull);
+	if (!isnull)
+	{
+		str_value = TextDatumGetCString(datum);
+		polstmt->with_check = stringToNode(str_value);
+
+		/* Adjust Vars to match new table's column numbering */
+		polstmt->with_check = map_variable_attnos(polstmt->with_check,
+													1, 0,
+													attmap,
+													InvalidOid,
+													&found_whole_row);
+		if (found_whole_row)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot convert whole-row table reference"),
+					errdetail("Security policy \"%s\" contains a whole-row reference to table \"%s\".",
+								NameStr(policy_form->polname),
+								RelationGetRelationName(parent_rel)));
+		pfree(str_value);
+	}
+
+	/*
+	 * The policy qual and check qual from the source table are already
+	 * transformed. We’ve copied them and adjusted the Vars, so no need to run
+	 * parse analysis again.
+	*/
+	polstmt->transformed = true;
+
+	return polstmt;
+}
+
 /*
  * get_collation		- fetch qualified name of a collation
  *
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index a8cc660d5e6..7b9124f11bf 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_POLICIES = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -3069,6 +3070,14 @@ typedef struct CreatePolicyStmt
 	Node	   *qual;			/* the policy's condition */
 	Node	   *with_check;		/* the policy's WITH CHECK condition. */
 	bool		transformed;	/* true when transformPolicyStmt is finished */
+	char	   *policycomment;	/* comment to apply to policies, or NULL */
+
+	/*
+	 * List of roles oids associated with the policy.  either this is NIL or
+	 * "roles" is NIL.
+	 * Currently used only in CREATE TABLE LIKE INCLUDING POLICIES.
+	 */
+	List	   *rolesId;
 } CreatePolicyStmt;
 
 /*----------------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index a4af3f717a1..3db1671d986 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -344,6 +344,7 @@ PG_KEYWORD("period", PERIOD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("policies", POLICIES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("preceding", PRECEDING, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 1dc8e5c8f42..02b3d6947d0 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -147,6 +147,85 @@ SELECT * FROM pg_policies WHERE schemaname = 'regress_rls_schema' AND tablename
  regress_rls_schema | document  | p2r        | RESTRICTIVE | {regress_rls_dave} | ALL | ((cid <> 44) AND (cid < 50))               | 
 (3 rows)
 
+--whole-row on qual, CREATE TABLE LIKE should fail.
+CREATE POLICY p2 ON document AS PERMISSIVE USING (document IS NOT NULL);
+CREATE TABLE document0(LIKE document INCLUDING ALL); --error
+ERROR:  cannot convert whole-row table reference
+DETAIL:  Security policy "p2" contains a whole-row reference to table "document".
+DROP POLICY p2 ON document;
+--whole-row on check qual, CREATE TABLE LIKE should fail.
+CREATE POLICY p3 ON document FOR INSERT WITH CHECK (document IS NOT NULL);
+CREATE TABLE document0(LIKE document INCLUDING ALL); --error
+ERROR:  cannot convert whole-row table reference
+DETAIL:  Security policy "p3" contains a whole-row reference to table "document".
+DROP POLICY p3 ON document;
+--a contrived complicated policy for testing expression node deparse
+CREATE POLICY p4 ON document AS PERMISSIVE USING (document.cid IS NOT NULL AND
+    (WITH cte AS (SELECT uaccount IS NOT NULL FROM uaccount)
+     SELECT * FROM cte WHERE EXISTS
+     (SELECT category FROM category WHERE EXISTS (SELECT uaccount FROM uaccount WHERE uaccount IS NULL))))
+    WITH CHECK (cid = (SELECT cid FROM document));
+COMMENT ON POLICY p1 ON document IS 'security policy comments';
+CREATE TABLE document1(LIKE document INCLUDING ALL EXCLUDING POLICIES);
+CREATE TABLE document2(prefix text, LIKE document INCLUDING COMMENTS INCLUDING POLICIES);
+--expect zero row
+SELECT 1 FROM pg_policies WHERE schemaname = 'regress_rls_schema' AND tablename = 'document1';
+ ?column? 
+----------
+(0 rows)
+
+SELECT tablename, policyname, permissive, roles, cmd, qual,with_check
+FROM    pg_policies
+WHERE   schemaname = 'regress_rls_schema' AND (tablename = 'document2' or tablename = 'document')
+ORDER BY policyname, tablename;
+ tablename | policyname | permissive  |       roles        | cmd |                               qual                                |           with_check           
+-----------+------------+-------------+--------------------+-----+-------------------------------------------------------------------+--------------------------------
+ document  | p1         | PERMISSIVE  | {public}           | ALL | (dlevel <= ( SELECT uaccount.seclv                               +| 
+           |            |             |                    |     |    FROM uaccount                                                 +| 
+           |            |             |                    |     |   WHERE (uaccount.pguser = CURRENT_USER)))                        | 
+ document2 | p1         | PERMISSIVE  | {public}           | ALL | (dlevel <= ( SELECT uaccount.seclv                               +| 
+           |            |             |                    |     |    FROM uaccount                                                 +| 
+           |            |             |                    |     |   WHERE (uaccount.pguser = CURRENT_USER)))                        | 
+ document  | p1r        | RESTRICTIVE | {regress_rls_dave} | ALL | (cid <> 44)                                                       | 
+ document2 | p1r        | RESTRICTIVE | {regress_rls_dave} | ALL | (cid <> 44)                                                       | 
+ document  | p2r        | RESTRICTIVE | {regress_rls_dave} | ALL | ((cid <> 44) AND (cid < 50))                                      | 
+ document2 | p2r        | RESTRICTIVE | {regress_rls_dave} | ALL | ((cid <> 44) AND (cid < 50))                                      | 
+ document  | p4         | PERMISSIVE  | {public}           | ALL | ((cid IS NOT NULL) AND ( WITH cte AS (                           +| (cid = ( SELECT document_1.cid+
+           |            |             |                    |     |          SELECT (uaccount.* IS NOT NULL) AS "?column?"           +|    FROM document document_1))
+           |            |             |                    |     |            FROM uaccount                                         +| 
+           |            |             |                    |     |         )                                                        +| 
+           |            |             |                    |     |  SELECT cte."?column?"                                           +| 
+           |            |             |                    |     |    FROM cte                                                      +| 
+           |            |             |                    |     |   WHERE (EXISTS ( SELECT category.*::category AS category        +| 
+           |            |             |                    |     |            FROM category                                         +| 
+           |            |             |                    |     |           WHERE (EXISTS ( SELECT uaccount.*::uaccount AS uaccount+| 
+           |            |             |                    |     |                    FROM uaccount                                 +| 
+           |            |             |                    |     |                   WHERE (uaccount.* IS NULL)))))))                | 
+ document2 | p4         | PERMISSIVE  | {public}           | ALL | ((cid IS NOT NULL) AND ( WITH cte AS (                           +| (cid = ( SELECT document.cid  +
+           |            |             |                    |     |          SELECT (uaccount.* IS NOT NULL) AS "?column?"           +|    FROM document))
+           |            |             |                    |     |            FROM uaccount                                         +| 
+           |            |             |                    |     |         )                                                        +| 
+           |            |             |                    |     |  SELECT cte."?column?"                                           +| 
+           |            |             |                    |     |    FROM cte                                                      +| 
+           |            |             |                    |     |   WHERE (EXISTS ( SELECT category.*::category AS category        +| 
+           |            |             |                    |     |            FROM category                                         +| 
+           |            |             |                    |     |           WHERE (EXISTS ( SELECT uaccount.*::uaccount AS uaccount+| 
+           |            |             |                    |     |                    FROM uaccount                                 +| 
+           |            |             |                    |     |                   WHERE (uaccount.* IS NULL)))))))                | 
+(8 rows)
+
+SELECT pd.description, pc.relname
+FROM pg_description pd JOIN pg_policy pp ON pp.oid = pd.objoid AND pp.tableoid = pd.classoid
+JOIN pg_class pc ON pc.oid = pp.polrelid
+WHERE relname IN ('document', 'document1', 'document2')
+ORDER BY relname COLLATE "C";
+       description        |  relname  
+--------------------------+-----------
+ security policy comments | document
+ security policy comments | document2
+(2 rows)
+
+DROP POLICY p4 ON document;
 -- viewpoint from regress_rls_bob
 SET SESSION AUTHORIZATION regress_rls_bob;
 SET row_security TO ON;
@@ -970,6 +1049,16 @@ SELECT * FROM pg_policies WHERE schemaname = 'regress_rls_schema' AND tablename
  regress_rls_schema | part_document | pp1r       | RESTRICTIVE | {regress_rls_dave} | ALL | (cid < 55)                                 | 
 (2 rows)
 
+CREATE TABLE part_document_copy(LIKE part_document INCLUDING POLICIES);
+SELECT * FROM pg_policies WHERE schemaname = 'regress_rls_schema' AND tablename = 'part_document_copy' ORDER BY policyname;
+     schemaname     |     tablename      | policyname | permissive  |       roles        | cmd |                    qual                    | with_check 
+--------------------+--------------------+------------+-------------+--------------------+-----+--------------------------------------------+------------
+ regress_rls_schema | part_document_copy | pp1        | PERMISSIVE  | {public}           | ALL | (dlevel <= ( SELECT uaccount.seclv        +| 
+                    |                    |            |             |                    |     |    FROM uaccount                          +| 
+                    |                    |            |             |                    |     |   WHERE (uaccount.pguser = CURRENT_USER))) | 
+ regress_rls_schema | part_document_copy | pp1r       | RESTRICTIVE | {regress_rls_dave} | ALL | (cid < 55)                                 | 
+(2 rows)
+
 -- viewpoint from regress_rls_bob
 SET SESSION AUTHORIZATION regress_rls_bob;
 SET row_security TO ON;
@@ -4006,6 +4095,17 @@ SELECT attname, most_common_vals FROM pg_stats
 BEGIN;
 CREATE TABLE coll_t (c) AS VALUES ('bar'::text);
 CREATE POLICY coll_p ON coll_t USING (c < ('foo'::text COLLATE "C"));
+-- coll_t1 row security is not enabled, but we still copy it's policies
+CREATE TABLE coll_t1(LIKE coll_t INCLUDING POLICIES);
+\d coll_t1
+       Table "regress_rls_schema.coll_t1"
+ Column | Type | Collation | Nullable | Default 
+--------+------+-----------+----------+---------
+ c      | text |           |          | 
+Policies (row security disabled):
+    POLICY "coll_p"
+      USING ((c < ('foo'::text COLLATE "C")))
+
 ALTER TABLE coll_t ENABLE ROW LEVEL SECURITY;
 GRANT SELECT ON coll_t TO regress_rls_alice;
 SELECT (string_to_array(polqual, ':'))[7] AS inputcollid FROM pg_policy WHERE polrelid = 'coll_t'::regclass;
@@ -4824,12 +4924,15 @@ drop table rls_t, test_t;
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 30 other objects
+NOTICE:  drop cascades to 33 other objects
 DETAIL:  drop cascades to function f_leak(text)
 drop cascades to table uaccount
 drop cascades to table category
 drop cascades to table document
+drop cascades to table document1
+drop cascades to table document2
 drop cascades to table part_document
+drop cascades to table part_document_copy
 drop cascades to table dependent
 drop cascades to table rec1
 drop cascades to table rec2
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 21ac0ca51ee..0eba61bbce6 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -111,6 +111,42 @@ CREATE POLICY p1r ON document AS RESTRICTIVE TO regress_rls_dave
 \d document
 SELECT * FROM pg_policies WHERE schemaname = 'regress_rls_schema' AND tablename = 'document' ORDER BY policyname;
 
+--whole-row on qual, CREATE TABLE LIKE should fail.
+CREATE POLICY p2 ON document AS PERMISSIVE USING (document IS NOT NULL);
+CREATE TABLE document0(LIKE document INCLUDING ALL); --error
+DROP POLICY p2 ON document;
+
+--whole-row on check qual, CREATE TABLE LIKE should fail.
+CREATE POLICY p3 ON document FOR INSERT WITH CHECK (document IS NOT NULL);
+CREATE TABLE document0(LIKE document INCLUDING ALL); --error
+DROP POLICY p3 ON document;
+
+--a contrived complicated policy for testing expression node deparse
+CREATE POLICY p4 ON document AS PERMISSIVE USING (document.cid IS NOT NULL AND
+    (WITH cte AS (SELECT uaccount IS NOT NULL FROM uaccount)
+     SELECT * FROM cte WHERE EXISTS
+     (SELECT category FROM category WHERE EXISTS (SELECT uaccount FROM uaccount WHERE uaccount IS NULL))))
+    WITH CHECK (cid = (SELECT cid FROM document));
+
+COMMENT ON POLICY p1 ON document IS 'security policy comments';
+CREATE TABLE document1(LIKE document INCLUDING ALL EXCLUDING POLICIES);
+CREATE TABLE document2(prefix text, LIKE document INCLUDING COMMENTS INCLUDING POLICIES);
+
+--expect zero row
+SELECT 1 FROM pg_policies WHERE schemaname = 'regress_rls_schema' AND tablename = 'document1';
+
+SELECT tablename, policyname, permissive, roles, cmd, qual,with_check
+FROM    pg_policies
+WHERE   schemaname = 'regress_rls_schema' AND (tablename = 'document2' or tablename = 'document')
+ORDER BY policyname, tablename;
+
+SELECT pd.description, pc.relname
+FROM pg_description pd JOIN pg_policy pp ON pp.oid = pd.objoid AND pp.tableoid = pd.classoid
+JOIN pg_class pc ON pc.oid = pp.polrelid
+WHERE relname IN ('document', 'document1', 'document2')
+ORDER BY relname COLLATE "C";
+DROP POLICY p4 ON document;
+
 -- viewpoint from regress_rls_bob
 SET SESSION AUTHORIZATION regress_rls_bob;
 SET row_security TO ON;
@@ -357,6 +393,8 @@ CREATE POLICY pp1r ON part_document AS RESTRICTIVE TO regress_rls_dave
 
 \d+ part_document
 SELECT * FROM pg_policies WHERE schemaname = 'regress_rls_schema' AND tablename like '%part_document%' ORDER BY policyname;
+CREATE TABLE part_document_copy(LIKE part_document INCLUDING POLICIES);
+SELECT * FROM pg_policies WHERE schemaname = 'regress_rls_schema' AND tablename = 'part_document_copy' ORDER BY policyname;
 
 -- viewpoint from regress_rls_bob
 SET SESSION AUTHORIZATION regress_rls_bob;
@@ -1770,6 +1808,11 @@ SELECT attname, most_common_vals FROM pg_stats
 BEGIN;
 CREATE TABLE coll_t (c) AS VALUES ('bar'::text);
 CREATE POLICY coll_p ON coll_t USING (c < ('foo'::text COLLATE "C"));
+
+-- coll_t1 row security is not enabled, but we still copy it's policies
+CREATE TABLE coll_t1(LIKE coll_t INCLUDING POLICIES);
+\d coll_t1
+
 ALTER TABLE coll_t ENABLE ROW LEVEL SECURITY;
 GRANT SELECT ON coll_t TO regress_rls_alice;
 SELECT (string_to_array(polqual, ':'))[7] AS inputcollid FROM pg_policy WHERE polrelid = 'coll_t'::regclass;
-- 
2.34.1

From 3d50beeef17915e715aed4d1dab2e0c250c1387d Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
Date: Mon, 15 Sep 2025 11:59:43 +0800
Subject: [PATCH v1 1/2] refactor CreatePolicy

discussion: https://postgr.es/m/
---
 src/backend/commands/policy.c             | 53 +++++++-------------
 src/backend/parser/gram.y                 |  1 +
 src/backend/parser/parse_utilcmd.c        | 59 +++++++++++++++++++++++
 src/backend/tcop/utility.c                |  2 +-
 src/include/commands/policy.h             |  2 +-
 src/include/nodes/parsenodes.h            |  1 +
 src/include/parser/parse_utilcmd.h        |  2 +
 src/test/regress/expected/rowsecurity.out |  2 +
 8 files changed, 85 insertions(+), 37 deletions(-)

diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 83056960fe4..799e1e3968a 100644
--- a/src/backend/commands/policy.c
+++ b/src/backend/commands/policy.c
@@ -33,6 +33,7 @@
 #include "parser/parse_collate.h"
 #include "parser/parse_node.h"
 #include "parser/parse_relation.h"
+#include "parser/parse_utilcmd.h"
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rowsecurity.h"
 #include "utils/acl.h"
@@ -566,7 +567,7 @@ RemoveRoleFromObjectPolicy(Oid roleid, Oid classid, Oid policy_id)
  * stmt - the CreatePolicyStmt that describes the policy to create.
  */
 ObjectAddress
-CreatePolicy(CreatePolicyStmt *stmt)
+CreatePolicy(CreatePolicyStmt *stmt, const char *queryString)
 {
 	Relation	pg_policy_rel;
 	Oid			policy_id;
@@ -576,8 +577,7 @@ CreatePolicy(CreatePolicyStmt *stmt)
 	Datum	   *role_oids;
 	int			nitems = 0;
 	ArrayType  *role_ids;
-	ParseState *qual_pstate;
-	ParseState *with_check_pstate;
+	ParseState *pstate;
 	ParseNamespaceItem *nsitem;
 	Node	   *qual;
 	Node	   *with_check_qual;
@@ -615,10 +615,6 @@ CreatePolicy(CreatePolicyStmt *stmt)
 	role_oids = policy_role_list_to_array(stmt->roles, &nitems);
 	role_ids = construct_array_builtin(role_oids, nitems, OIDOID);
 
-	/* Parse the supplied clause */
-	qual_pstate = make_parsestate(NULL);
-	with_check_pstate = make_parsestate(NULL);
-
 	/* zero-clear */
 	memset(values, 0, sizeof(values));
 	memset(isnull, 0, sizeof(isnull));
@@ -628,35 +624,23 @@ CreatePolicy(CreatePolicyStmt *stmt)
 										0,
 										RangeVarCallbackForPolicy,
 										stmt);
+	if (!stmt->transformed)
+		stmt = transformPolicyStmt(table_id, stmt, queryString);
 
-	/* Open target_table to build quals. No additional lock is necessary. */
+	qual = stmt->qual;
+	with_check_qual = stmt->with_check;
+
+	/* we'll need the pstate->rtable for recordDependencyOnExpr */
+	pstate = make_parsestate(NULL);
+	pstate->p_sourcetext = queryString;
+
+	/* No additional lock is necessary. */
 	target_table = relation_open(table_id, NoLock);
 
-	/* Add for the regular security quals */
-	nsitem = addRangeTableEntryForRelation(qual_pstate, target_table,
+	nsitem = addRangeTableEntryForRelation(pstate, target_table,
 										   AccessShareLock,
 										   NULL, false, false);
-	addNSItemToQuery(qual_pstate, nsitem, false, true, true);
-
-	/* Add for the with-check quals */
-	nsitem = addRangeTableEntryForRelation(with_check_pstate, target_table,
-										   AccessShareLock,
-										   NULL, false, false);
-	addNSItemToQuery(with_check_pstate, nsitem, false, true, true);
-
-	qual = transformWhereClause(qual_pstate,
-								stmt->qual,
-								EXPR_KIND_POLICY,
-								"POLICY");
-
-	with_check_qual = transformWhereClause(with_check_pstate,
-										   stmt->with_check,
-										   EXPR_KIND_POLICY,
-										   "POLICY");
-
-	/* Fix up collation information */
-	assign_expr_collations(qual_pstate, qual);
-	assign_expr_collations(with_check_pstate, with_check_qual);
+	addNSItemToQuery(pstate, nsitem, false, true, true);
 
 	/* Open pg_policy catalog */
 	pg_policy_rel = table_open(PolicyRelationId, RowExclusiveLock);
@@ -724,11 +708,11 @@ CreatePolicy(CreatePolicyStmt *stmt)
 
 	recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
 
-	recordDependencyOnExpr(&myself, qual, qual_pstate->p_rtable,
+	recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
 						   DEPENDENCY_NORMAL);
 
 	recordDependencyOnExpr(&myself, with_check_qual,
-						   with_check_pstate->p_rtable, DEPENDENCY_NORMAL);
+						   pstate->p_rtable, DEPENDENCY_NORMAL);
 
 	/* Register role dependencies */
 	target.classId = AuthIdRelationId;
@@ -749,8 +733,7 @@ CreatePolicy(CreatePolicyStmt *stmt)
 
 	/* Clean up. */
 	heap_freetuple(policy_tuple);
-	free_parsestate(qual_pstate);
-	free_parsestate(with_check_pstate);
+	free_parsestate(pstate);
 	systable_endscan(sscan);
 	relation_close(target_table, NoLock);
 	table_close(pg_policy_rel, RowExclusiveLock);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9fd48acb1f8..8016c58b49c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -5948,6 +5948,7 @@ CreatePolicyStmt:
 					n->roles = $8;
 					n->qual = $9;
 					n->with_check = $10;
+					n->transformed = false;
 					$$ = (Node *) n;
 				}
 		;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index e96b38a59d5..394a037e817 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3205,6 +3205,65 @@ transformStatsStmt(Oid relid, CreateStatsStmt *stmt, const char *queryString)
 	return stmt;
 }
 
+/*
+ * transformPolicyStmt - parse analysis for CREATE POLICY
+ * mainly parse analysis for qual and check qual of the policy.
+ *
+ * To avoid race conditions, it's important that this function relies only on
+ * the passed-in relid (and not on stmt->table) to determine the target
+ * relation.
+ */
+CreatePolicyStmt *
+transformPolicyStmt(Oid relid, CreatePolicyStmt *stmt, const char *queryString)
+{
+	ParseState *pstate;
+	ParseNamespaceItem *nsitem;
+	Relation	rel;
+
+	/* Nothing to do if statement already transformed. */
+	if (stmt->transformed)
+		return stmt;
+
+	/* Set up pstate */
+	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);
+	nsitem = addRangeTableEntryForRelation(pstate, rel,
+										   AccessShareLock,
+										   NULL, false, true);
+
+	/* no to join list, yes to namespaces */
+	addNSItemToQuery(pstate, nsitem, false, true, true);
+
+	stmt->qual = transformWhereClause(pstate,
+									  stmt->qual,
+									  EXPR_KIND_POLICY,
+									  "POLICY");
+
+	stmt->with_check = transformWhereClause(pstate,
+											stmt->with_check,
+											EXPR_KIND_POLICY,
+											"POLICY");
+	/* Fix up collation information */
+	assign_expr_collations(pstate, stmt->qual);
+	assign_expr_collations(pstate, stmt->with_check);
+
+	free_parsestate(pstate);
+
+	/* Close relation */
+	table_close(rel, NoLock);
+
+	/* Mark statement as successfully transformed */
+	stmt->transformed = true;
+
+	return stmt;
+}
 
 /*
  * transformRuleStmt -
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 5f442bc3bd4..e8b3deca825 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1818,7 +1818,7 @@ ProcessUtilitySlow(ParseState *pstate,
 				break;
 
 			case T_CreatePolicyStmt:	/* CREATE POLICY */
-				address = CreatePolicy((CreatePolicyStmt *) parsetree);
+				address = CreatePolicy((CreatePolicyStmt *) parsetree, queryString);
 				break;
 
 			case T_AlterPolicyStmt: /* ALTER POLICY */
diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h
index f06aa1df439..dab4030c38d 100644
--- a/src/include/commands/policy.h
+++ b/src/include/commands/policy.h
@@ -25,7 +25,7 @@ extern void RemovePolicyById(Oid policy_id);
 
 extern bool RemoveRoleFromObjectPolicy(Oid roleid, Oid classid, Oid policy_id);
 
-extern ObjectAddress CreatePolicy(CreatePolicyStmt *stmt);
+extern ObjectAddress CreatePolicy(CreatePolicyStmt *stmt, const char *queryString);
 extern ObjectAddress AlterPolicy(AlterPolicyStmt *stmt);
 
 extern Oid	get_relation_policy_oid(Oid relid, const char *policy_name,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..a8cc660d5e6 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3068,6 +3068,7 @@ typedef struct CreatePolicyStmt
 	List	   *roles;			/* the roles associated with the policy */
 	Node	   *qual;			/* the policy's condition */
 	Node	   *with_check;		/* the policy's WITH CHECK condition. */
+	bool		transformed;	/* true when transformPolicyStmt is finished */
 } CreatePolicyStmt;
 
 /*----------------------
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 9f2b58de797..7a3562b88c2 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 CreatePolicyStmt *transformPolicyStmt(Oid relid, CreatePolicyStmt *stmt,
+											 const char *queryString);
 extern void transformRuleStmt(RuleStmt *stmt, const char *queryString,
 							  List **actions, Node **whereClause);
 extern List *transformCreateSchemaStmtElements(List *schemaElts,
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 8c879509313..1dc8e5c8f42 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -4084,6 +4084,8 @@ BEGIN;
 CREATE TABLE t (c) AS VALUES ('bar'::text);
 CREATE POLICY p ON t USING (max(c)); -- fails: aggregate functions are not allowed in policy expressions
 ERROR:  aggregate functions are not allowed in policy expressions
+LINE 1: CREATE POLICY p ON t USING (max(c));
+                                    ^
 ROLLBACK;
 --
 -- Non-target relations are only subject to SELECT policies
-- 
2.34.1

Reply via email to