public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH v3 1/2] Secondary index access optimizations
44+ messages / 12 participants
[nested] [flat]

* [PATCH v3 1/2] Secondary index access optimizations
@ 2018-10-12 12:53  Konstantin Knizhnik <[email protected]>
  0 siblings, 0 replies; 44+ messages in thread

From: Konstantin Knizhnik @ 2018-10-12 12:53 UTC (permalink / raw)

---
 .../postgres_fdw/expected/postgres_fdw.out    |   8 +-
 contrib/postgres_fdw/sql/postgres_fdw.sql     |   2 +-
 src/backend/optimizer/path/allpaths.c         |   2 +
 src/backend/optimizer/util/plancat.c          |  45 ++
 src/include/optimizer/plancat.h               |   3 +
 src/test/regress/expected/create_table.out    |  14 +-
 src/test/regress/expected/inherit.out         | 123 ++--
 .../regress/expected/partition_aggregate.out  |  10 +-
 src/test/regress/expected/partition_join.out  |  42 +-
 src/test/regress/expected/partition_prune.out | 587 ++++++------------
 src/test/regress/expected/rowsecurity.out     |  12 +-
 src/test/regress/expected/update.out          |   4 +-
 12 files changed, 322 insertions(+), 530 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..dbbae1820e 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -629,12 +629,12 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 IS NULL;        -- Nu
    Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" IS NULL))
 (3 rows)
 
-EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL;    -- NullTest
-                                             QUERY PLAN                                              
------------------------------------------------------------------------------------------------------
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL and c3 is not null;    -- NullTest
+                                            QUERY PLAN                                            
+--------------------------------------------------------------------------------------------------
  Foreign Scan on public.ft1 t1
    Output: c1, c2, c3, c4, c5, c6, c7, c8
-   Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" IS NOT NULL))
+   Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((c3 IS NOT NULL))
 (3 rows)
 
 EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..08aef9289e 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -304,7 +304,7 @@ RESET enable_nestloop;
 EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 1;         -- Var, OpExpr(b), Const
 EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 100 AND t1.c2 = 0; -- BoolExpr
 EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 IS NULL;        -- NullTest
-EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL;    -- NullTest
+EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 IS NOT NULL and c3 is not null;    -- NullTest
 EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr
 EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 = -c1;          -- OpExpr(l)
 EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE 1 = c1!;           -- OpExpr(r)
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 6da0dcd61c..a9171c075c 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -387,6 +387,7 @@ set_rel_size(PlannerInfo *root, RelOptInfo *rel,
 		switch (rel->rtekind)
 		{
 			case RTE_RELATION:
+				remove_restrictions_implied_by_constraints(root, rel, rte);
 				if (rte->relkind == RELKIND_FOREIGN_TABLE)
 				{
 					/* Foreign table */
@@ -1040,6 +1041,7 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
 			set_dummy_rel_pathlist(childrel);
 			continue;
 		}
+		remove_restrictions_implied_by_constraints(root, childrel, childRTE);
 
 		/*
 		 * Constraint exclusion failed, so copy the parent's join quals and
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 25545029d7..45cd72a0fe 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1557,6 +1557,51 @@ relation_excluded_by_constraints(PlannerInfo *root,
 	return false;
 }
 
+/*
+ * Remove from restrictions list items implied by table constraints
+ */
+void remove_restrictions_implied_by_constraints(PlannerInfo *root,
+												RelOptInfo *rel, RangeTblEntry *rte)
+{
+	List	   *constraint_pred;
+	List	   *safe_constraints = NIL;
+	List	   *safe_restrictions = NIL;
+	ListCell   *lc;
+
+	if (rte->rtekind != RTE_RELATION || rte->inh)
+		return;
+
+	/*
+	 * OK to fetch the constraint expressions.  Include "col IS NOT NULL"
+	 * expressions for attnotnull columns, in case we can refute those.
+	 */
+	constraint_pred = get_relation_constraints(root, rte->relid, rel, true, true, true);
+
+	/*
+	 * We do not currently enforce that CHECK constraints contain only
+	 * immutable functions, so it's necessary to check here. We daren't draw
+	 * conclusions from plan-time evaluation of non-immutable functions. Since
+	 * they're ANDed, we can just ignore any mutable constraints in the list,
+	 * and reason about the rest.
+	 */
+	foreach(lc, constraint_pred)
+	{
+		Node	   *pred = (Node*) lfirst(lc);
+
+		if (!contain_mutable_functions(pred))
+			safe_constraints = lappend(safe_constraints, pred);
+	}
+
+	foreach(lc, rel->baserestrictinfo)
+	{
+		RestrictInfo *rinfo = lfirst_node(RestrictInfo, lc);
+		if (!predicate_implied_by(list_make1(rinfo->clause), safe_constraints, false)) {
+			safe_restrictions = lappend(safe_restrictions, rinfo);
+		}
+	}
+	rel->baserestrictinfo = safe_restrictions;
+}
+
 
 /*
  * build_physical_tlist
diff --git a/src/include/optimizer/plancat.h b/src/include/optimizer/plancat.h
index c29a7091ec..792c809ed3 100644
--- a/src/include/optimizer/plancat.h
+++ b/src/include/optimizer/plancat.h
@@ -39,6 +39,9 @@ extern int32 get_relation_data_width(Oid relid, int32 *attr_widths);
 extern bool relation_excluded_by_constraints(PlannerInfo *root,
 											 RelOptInfo *rel, RangeTblEntry *rte);
 
+extern void remove_restrictions_implied_by_constraints(PlannerInfo *root,
+													   RelOptInfo *rel, RangeTblEntry *rte);
+
 extern List *build_physical_tlist(PlannerInfo *root, RelOptInfo *rel);
 
 extern bool has_unique_index(RelOptInfo *rel, AttrNumber attno);
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index 1c72f23bc9..59cd42f48d 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -529,11 +529,10 @@ create table partitioned2
   partition of partitioned for values in ('(2,4)'::partitioned);
 explain (costs off)
 select * from partitioned where row(a,b)::partitioned = '(1,2)'::partitioned;
-                        QUERY PLAN                         
------------------------------------------------------------
+              QUERY PLAN              
+--------------------------------------
  Seq Scan on partitioned1 partitioned
-   Filter: (ROW(a, b)::partitioned = '(1,2)'::partitioned)
-(2 rows)
+(1 row)
 
 drop table partitioned;
 -- whole-row Var in partition key works too
@@ -545,11 +544,10 @@ create table partitioned2
   partition of partitioned for values in ('(2,4)');
 explain (costs off)
 select * from partitioned where partitioned = '(1,2)'::partitioned;
-                           QUERY PLAN                            
------------------------------------------------------------------
+              QUERY PLAN              
+--------------------------------------
  Seq Scan on partitioned1 partitioned
-   Filter: ((partitioned.*)::partitioned = '(1,2)'::partitioned)
-(2 rows)
+(1 row)
 
 \d+ partitioned1
                                Table "public.partitioned1"
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 2b68aef654..d76f3d462f 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1800,29 +1800,25 @@ explain (costs off) select * from list_parted where a is not null;
 ----------------------------------------------
  Append
    ->  Seq Scan on part_ab_cd list_parted_1
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on part_ef_gh list_parted_2
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on part_null_xy list_parted_3
          Filter: (a IS NOT NULL)
-(7 rows)
+(5 rows)
 
 explain (costs off) select * from list_parted where a in ('ab', 'cd', 'ef');
                         QUERY PLAN                        
 ----------------------------------------------------------
  Append
    ->  Seq Scan on part_ab_cd list_parted_1
-         Filter: ((a)::text = ANY ('{ab,cd,ef}'::text[]))
    ->  Seq Scan on part_ef_gh list_parted_2
          Filter: ((a)::text = ANY ('{ab,cd,ef}'::text[]))
-(5 rows)
+(4 rows)
 
 explain (costs off) select * from list_parted where a = 'ab' or a in (null, 'cd');
-                                   QUERY PLAN                                    
----------------------------------------------------------------------------------
+             QUERY PLAN             
+------------------------------------
  Seq Scan on part_ab_cd list_parted
-   Filter: (((a)::text = 'ab'::text) OR ((a)::text = ANY ('{NULL,cd}'::text[])))
-(2 rows)
+(1 row)
 
 explain (costs off) select * from list_parted where a = 'ab';
              QUERY PLAN             
@@ -1878,26 +1874,21 @@ explain (costs off) select * from range_list_parted where b = 'ab';
 ------------------------------------------------------
  Append
    ->  Seq Scan on part_1_10_ab range_list_parted_1
-         Filter: (b = 'ab'::bpchar)
    ->  Seq Scan on part_10_20_ab range_list_parted_2
-         Filter: (b = 'ab'::bpchar)
    ->  Seq Scan on part_21_30_ab range_list_parted_3
-         Filter: (b = 'ab'::bpchar)
    ->  Seq Scan on part_40_inf_ab range_list_parted_4
-         Filter: (b = 'ab'::bpchar)
-(9 rows)
+(5 rows)
 
 explain (costs off) select * from range_list_parted where a between 3 and 23 and b in ('ab');
-                           QUERY PLAN                            
------------------------------------------------------------------
+                     QUERY PLAN                      
+-----------------------------------------------------
  Append
    ->  Seq Scan on part_1_10_ab range_list_parted_1
-         Filter: ((a >= 3) AND (a <= 23) AND (b = 'ab'::bpchar))
+         Filter: (a >= 3)
    ->  Seq Scan on part_10_20_ab range_list_parted_2
-         Filter: ((a >= 3) AND (a <= 23) AND (b = 'ab'::bpchar))
    ->  Seq Scan on part_21_30_ab range_list_parted_3
-         Filter: ((a >= 3) AND (a <= 23) AND (b = 'ab'::bpchar))
-(7 rows)
+         Filter: (a <= 23)
+(6 rows)
 
 /* Should select no rows because range partition key cannot be null */
 explain (costs off) select * from range_list_parted where a is null;
@@ -1912,44 +1903,34 @@ explain (costs off) select * from range_list_parted where b is null;
                    QUERY PLAN                   
 ------------------------------------------------
  Seq Scan on part_40_inf_null range_list_parted
-   Filter: (b IS NULL)
-(2 rows)
+(1 row)
 
 explain (costs off) select * from range_list_parted where a is not null and a < 67;
                        QUERY PLAN                       
 --------------------------------------------------------
  Append
    ->  Seq Scan on part_1_10_ab range_list_parted_1
-         Filter: ((a IS NOT NULL) AND (a < 67))
    ->  Seq Scan on part_1_10_cd range_list_parted_2
-         Filter: ((a IS NOT NULL) AND (a < 67))
    ->  Seq Scan on part_10_20_ab range_list_parted_3
-         Filter: ((a IS NOT NULL) AND (a < 67))
    ->  Seq Scan on part_10_20_cd range_list_parted_4
-         Filter: ((a IS NOT NULL) AND (a < 67))
    ->  Seq Scan on part_21_30_ab range_list_parted_5
-         Filter: ((a IS NOT NULL) AND (a < 67))
    ->  Seq Scan on part_21_30_cd range_list_parted_6
-         Filter: ((a IS NOT NULL) AND (a < 67))
    ->  Seq Scan on part_40_inf_ab range_list_parted_7
-         Filter: ((a IS NOT NULL) AND (a < 67))
+         Filter: (a < 67)
    ->  Seq Scan on part_40_inf_cd range_list_parted_8
-         Filter: ((a IS NOT NULL) AND (a < 67))
+         Filter: (a < 67)
    ->  Seq Scan on part_40_inf_null range_list_parted_9
-         Filter: ((a IS NOT NULL) AND (a < 67))
-(19 rows)
+         Filter: (a < 67)
+(13 rows)
 
 explain (costs off) select * from range_list_parted where a >= 30;
                        QUERY PLAN                       
 --------------------------------------------------------
  Append
    ->  Seq Scan on part_40_inf_ab range_list_parted_1
-         Filter: (a >= 30)
    ->  Seq Scan on part_40_inf_cd range_list_parted_2
-         Filter: (a >= 30)
    ->  Seq Scan on part_40_inf_null range_list_parted_3
-         Filter: (a >= 30)
-(7 rows)
+(4 rows)
 
 drop table list_parted;
 drop table range_list_parted;
@@ -1990,7 +1971,7 @@ explain (costs off) select * from mcrparted where a = 10 and abs(b) = 5;	-- scan
    ->  Seq Scan on mcrparted1 mcrparted_1
          Filter: ((a = 10) AND (abs(b) = 5))
    ->  Seq Scan on mcrparted2 mcrparted_2
-         Filter: ((a = 10) AND (abs(b) = 5))
+         Filter: (abs(b) = 5)
    ->  Seq Scan on mcrparted_def mcrparted_3
          Filter: ((a = 10) AND (abs(b) = 5))
 (7 rows)
@@ -2022,24 +2003,19 @@ explain (costs off) select * from mcrparted where a > -1;	-- scans all partition
    ->  Seq Scan on mcrparted0 mcrparted_1
          Filter: (a > '-1'::integer)
    ->  Seq Scan on mcrparted1 mcrparted_2
-         Filter: (a > '-1'::integer)
    ->  Seq Scan on mcrparted2 mcrparted_3
-         Filter: (a > '-1'::integer)
    ->  Seq Scan on mcrparted3 mcrparted_4
-         Filter: (a > '-1'::integer)
    ->  Seq Scan on mcrparted4 mcrparted_5
-         Filter: (a > '-1'::integer)
    ->  Seq Scan on mcrparted5 mcrparted_6
-         Filter: (a > '-1'::integer)
    ->  Seq Scan on mcrparted_def mcrparted_7
          Filter: (a > '-1'::integer)
-(15 rows)
+(10 rows)
 
 explain (costs off) select * from mcrparted where a = 20 and abs(b) = 10 and c > 10;	-- scans mcrparted4
-                     QUERY PLAN                      
------------------------------------------------------
+               QUERY PLAN               
+----------------------------------------
  Seq Scan on mcrparted4 mcrparted
-   Filter: ((c > 10) AND (a = 20) AND (abs(b) = 10))
+   Filter: ((c > 10) AND (abs(b) = 10))
 (2 rows)
 
 explain (costs off) select * from mcrparted where a = 20 and c > 20; -- scans mcrparted3, mcrparte4, mcrparte5, mcrparted_def
@@ -2049,7 +2025,7 @@ explain (costs off) select * from mcrparted where a = 20 and c > 20; -- scans mc
    ->  Seq Scan on mcrparted3 mcrparted_1
          Filter: ((c > 20) AND (a = 20))
    ->  Seq Scan on mcrparted4 mcrparted_2
-         Filter: ((c > 20) AND (a = 20))
+         Filter: (c > 20)
    ->  Seq Scan on mcrparted5 mcrparted_3
          Filter: ((c > 20) AND (a = 20))
    ->  Seq Scan on mcrparted_def mcrparted_4
@@ -2069,11 +2045,11 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
    InitPlan 1 (returns $0)
      ->  Limit
            ->  Index Only Scan using parted_minmax1i on parted_minmax1 parted_minmax
-                 Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
+                 Index Cond: (b = '12345'::text)
    InitPlan 2 (returns $1)
      ->  Limit
            ->  Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax_1
-                 Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
+                 Index Cond: (b = '12345'::text)
 (9 rows)
 
 select min(a), max(a) from parted_minmax where b = '12345';
@@ -2173,14 +2149,11 @@ explain (costs off) select * from mcrparted where a < 20 order by a, abs(b), c;
 -------------------------------------------------------------------------
  Append
    ->  Index Scan using mcrparted0_a_abs_c_idx on mcrparted0 mcrparted_1
-         Index Cond: (a < 20)
    ->  Index Scan using mcrparted1_a_abs_c_idx on mcrparted1 mcrparted_2
-         Index Cond: (a < 20)
    ->  Index Scan using mcrparted2_a_abs_c_idx on mcrparted2 mcrparted_3
-         Index Cond: (a < 20)
    ->  Index Scan using mcrparted3_a_abs_c_idx on mcrparted3 mcrparted_4
          Index Cond: (a < 20)
-(9 rows)
+(6 rows)
 
 create table mclparted (a int) partition by list(a);
 create table mclparted1 partition of mclparted for values in(1);
@@ -2226,14 +2199,11 @@ explain (costs off) select * from mcrparted where a < 20 order by a, abs(b), c l
          ->  Sort
                Sort Key: mcrparted_1.a, (abs(mcrparted_1.b)), mcrparted_1.c
                ->  Seq Scan on mcrparted0 mcrparted_1
-                     Filter: (a < 20)
          ->  Index Scan using mcrparted1_a_abs_c_idx on mcrparted1 mcrparted_2
-               Index Cond: (a < 20)
          ->  Index Scan using mcrparted2_a_abs_c_idx on mcrparted2 mcrparted_3
-               Index Cond: (a < 20)
          ->  Index Scan using mcrparted3_a_abs_c_idx on mcrparted3 mcrparted_4
                Index Cond: (a < 20)
-(12 rows)
+(9 rows)
 
 set enable_bitmapscan = 0;
 -- Ensure Append node can be used when the partition is ordered by some
@@ -2245,8 +2215,7 @@ explain (costs off) select * from mcrparted where a = 10 order by a, abs(b), c;
    ->  Index Scan using mcrparted1_a_abs_c_idx on mcrparted1 mcrparted_1
          Index Cond: (a = 10)
    ->  Index Scan using mcrparted2_a_abs_c_idx on mcrparted2 mcrparted_2
-         Index Cond: (a = 10)
-(5 rows)
+(4 rows)
 
 reset enable_bitmapscan;
 drop table mcrparted;
@@ -2276,39 +2245,35 @@ explain (costs off) select * from bool_rp where b = true order by b,a;
 ----------------------------------------------------------------------------------
  Append
    ->  Index Only Scan using bool_rp_true_1k_b_a_idx on bool_rp_true_1k bool_rp_1
-         Index Cond: (b = true)
    ->  Index Only Scan using bool_rp_true_2k_b_a_idx on bool_rp_true_2k bool_rp_2
-         Index Cond: (b = true)
-(5 rows)
+(3 rows)
 
 explain (costs off) select * from bool_rp where b = false order by b,a;
                                      QUERY PLAN                                     
 ------------------------------------------------------------------------------------
  Append
    ->  Index Only Scan using bool_rp_false_1k_b_a_idx on bool_rp_false_1k bool_rp_1
-         Index Cond: (b = false)
    ->  Index Only Scan using bool_rp_false_2k_b_a_idx on bool_rp_false_2k bool_rp_2
-         Index Cond: (b = false)
-(5 rows)
+(3 rows)
 
 explain (costs off) select * from bool_rp where b = true order by a;
-                                    QUERY PLAN                                    
-----------------------------------------------------------------------------------
- Append
-   ->  Index Only Scan using bool_rp_true_1k_b_a_idx on bool_rp_true_1k bool_rp_1
-         Index Cond: (b = true)
-   ->  Index Only Scan using bool_rp_true_2k_b_a_idx on bool_rp_true_2k bool_rp_2
-         Index Cond: (b = true)
+                    QUERY PLAN                     
+---------------------------------------------------
+ Sort
+   Sort Key: bool_rp.a
+   ->  Append
+         ->  Seq Scan on bool_rp_true_1k bool_rp_1
+         ->  Seq Scan on bool_rp_true_2k bool_rp_2
 (5 rows)
 
 explain (costs off) select * from bool_rp where b = false order by a;
-                                     QUERY PLAN                                     
-------------------------------------------------------------------------------------
- Append
-   ->  Index Only Scan using bool_rp_false_1k_b_a_idx on bool_rp_false_1k bool_rp_1
-         Index Cond: (b = false)
-   ->  Index Only Scan using bool_rp_false_2k_b_a_idx on bool_rp_false_2k bool_rp_2
-         Index Cond: (b = false)
+                     QUERY PLAN                     
+----------------------------------------------------
+ Sort
+   Sort Key: bool_rp.a
+   ->  Append
+         ->  Seq Scan on bool_rp_false_1k bool_rp_1
+         ->  Seq Scan on bool_rp_false_2k bool_rp_2
 (5 rows)
 
 drop table bool_rp;
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index 45c698daf4..ebfdf15fb0 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -738,16 +738,13 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a LEFT JOI
                Filter: ((pagg_tab1.x > 5) OR (pagg_tab2.y < 20))
                ->  Append
                      ->  Seq Scan on pagg_tab1_p1 pagg_tab1_1
-                           Filter: (x < 20)
                      ->  Seq Scan on pagg_tab1_p2 pagg_tab1_2
-                           Filter: (x < 20)
                ->  Hash
                      ->  Append
                            ->  Seq Scan on pagg_tab2_p2 pagg_tab2_1
                                  Filter: (y > 10)
                            ->  Seq Scan on pagg_tab2_p3 pagg_tab2_2
-                                 Filter: (y > 10)
-(18 rows)
+(15 rows)
 
 SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a LEFT JOIN (SELECT * FROM pagg_tab2 WHERE y > 10) b ON a.x = b.y WHERE a.x > 5 or b.y < 20  GROUP BY a.x, b.y ORDER BY 1, 2;
  x  | y  | count 
@@ -778,16 +775,13 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
                Filter: ((pagg_tab1.x > 5) OR (pagg_tab2.y < 20))
                ->  Append
                      ->  Seq Scan on pagg_tab1_p1 pagg_tab1_1
-                           Filter: (x < 20)
                      ->  Seq Scan on pagg_tab1_p2 pagg_tab1_2
-                           Filter: (x < 20)
                ->  Hash
                      ->  Append
                            ->  Seq Scan on pagg_tab2_p2 pagg_tab2_1
                                  Filter: (y > 10)
                            ->  Seq Scan on pagg_tab2_p3 pagg_tab2_2
-                                 Filter: (y > 10)
-(18 rows)
+(15 rows)
 
 SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOIN (SELECT * FROM pagg_tab2 WHERE y > 10) b ON a.x = b.y WHERE a.x > 5 or b.y < 20 GROUP BY a.x, b.y ORDER BY 1, 2;
  x  | y  | count 
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 585e724375..36b92ec398 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -218,14 +218,13 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a < 450) t1 LEFT JO
                ->  Seq Scan on prt2_p2 prt2_1
                      Filter: (b > 250)
                ->  Seq Scan on prt2_p3 prt2_2
-                     Filter: (b > 250)
          ->  Hash
                ->  Append
                      ->  Seq Scan on prt1_p1 prt1_1
-                           Filter: ((a < 450) AND (b = 0))
+                           Filter: (b = 0)
                      ->  Seq Scan on prt1_p2 prt1_2
                            Filter: ((a < 450) AND (b = 0))
-(15 rows)
+(14 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a < 450) t1 LEFT JOIN (SELECT * FROM prt2 WHERE b > 250) t2 ON t1.a = t2.b WHERE t1.b = 0 ORDER BY t1.a, t2.b;
   a  |  c   |  b  |  c   
@@ -253,7 +252,6 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a < 450) t1 FULL JO
          Filter: ((prt1.b = 0) OR (prt2.a = 0))
          ->  Append
                ->  Seq Scan on prt1_p1 prt1_1
-                     Filter: (a < 450)
                ->  Seq Scan on prt1_p2 prt1_2
                      Filter: (a < 450)
          ->  Hash
@@ -261,8 +259,7 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a < 450) t1 FULL JO
                      ->  Seq Scan on prt2_p2 prt2_1
                            Filter: (b > 250)
                      ->  Seq Scan on prt2_p3 prt2_2
-                           Filter: (b > 250)
-(16 rows)
+(14 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a < 450) t1 FULL JOIN (SELECT * FROM prt2 WHERE b > 250) t2 ON t1.a = t2.b WHERE t1.b = 0 OR t2.a = 0 ORDER BY t1.a, t2.b;
   a  |  c   |  b  |  c   
@@ -1181,7 +1178,7 @@ SELECT t1.a, t2.b FROM (SELECT * FROM prt1 WHERE a < 450) t1 LEFT JOIN (SELECT *
                Sort Key: prt1.a
                ->  Append
                      ->  Seq Scan on prt1_p1 prt1_1
-                           Filter: ((a < 450) AND (b = 0))
+                           Filter: (b = 0)
                      ->  Seq Scan on prt1_p2 prt1_2
                            Filter: ((a < 450) AND (b = 0))
          ->  Sort
@@ -1190,8 +1187,7 @@ SELECT t1.a, t2.b FROM (SELECT * FROM prt1 WHERE a < 450) t1 LEFT JOIN (SELECT *
                      ->  Seq Scan on prt2_p2 prt2_1
                            Filter: (b > 250)
                      ->  Seq Scan on prt2_p3 prt2_2
-                           Filter: (b > 250)
-(18 rows)
+(17 rows)
 
 SELECT t1.a, t2.b FROM (SELECT * FROM prt1 WHERE a < 450) t1 LEFT JOIN (SELECT * FROM prt2 WHERE b > 250) t2 ON t1.a = t2.b WHERE t1.b = 0 ORDER BY t1.a, t2.b;
   a  |  b  
@@ -2197,7 +2193,7 @@ where not exists (select 1 from prtx2
  Append
    ->  Nested Loop Anti Join
          ->  Seq Scan on prtx1_1
-               Filter: ((a < 20) AND (c = 120))
+               Filter: (c = 120)
          ->  Bitmap Heap Scan on prtx2_1
                Recheck Cond: ((b = prtx1_1.b) AND (c = 123))
                Filter: (a = prtx1_1.a)
@@ -2238,7 +2234,7 @@ where not exists (select 1 from prtx2
  Append
    ->  Nested Loop Anti Join
          ->  Seq Scan on prtx1_1
-               Filter: ((a < 20) AND (c = 91))
+               Filter: (c = 91)
          ->  Bitmap Heap Scan on prtx2_1
                Recheck Cond: ((b = (prtx1_1.b + 1)) OR (c = 99))
                Filter: (a = prtx1_1.a)
@@ -3102,8 +3098,8 @@ INSERT INTO prt2_adv SELECT i % 25, i, to_char(i, 'FM0000') FROM generate_series
 ANALYZE prt2_adv;
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_adv t1 INNER JOIN prt2_adv t2 ON (t1.a = t2.b) WHERE t1.a < 300 AND t1.b = 0 ORDER BY t1.a, t2.b;
-                        QUERY PLAN                         
------------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  Sort
    Sort Key: t1.a
    ->  Append
@@ -3112,13 +3108,13 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_adv t1 INNER JOIN prt2_adv t2 ON (t1.a =
                ->  Seq Scan on prt2_adv_p1 t2_1
                ->  Hash
                      ->  Seq Scan on prt1_adv_p1 t1_1
-                           Filter: ((a < 300) AND (b = 0))
+                           Filter: (b = 0)
          ->  Hash Join
                Hash Cond: (t2_2.b = t1_2.a)
                ->  Seq Scan on prt2_adv_p2 t2_2
                ->  Hash
                      ->  Seq Scan on prt1_adv_p2 t1_2
-                           Filter: ((a < 300) AND (b = 0))
+                           Filter: (b = 0)
 (15 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_adv t1 INNER JOIN prt2_adv t2 ON (t1.a = t2.b) WHERE t1.a < 300 AND t1.b = 0 ORDER BY t1.a, t2.b;
@@ -3141,8 +3137,8 @@ CREATE TABLE prt2_adv_default PARTITION OF prt2_adv DEFAULT;
 ANALYZE prt2_adv;
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_adv t1 INNER JOIN prt2_adv t2 ON (t1.a = t2.b) WHERE t1.a >= 100 AND t1.a < 300 AND t1.b = 0 ORDER BY t1.a, t2.b;
-                                QUERY PLAN                                
---------------------------------------------------------------------------
+                      QUERY PLAN                      
+------------------------------------------------------
  Sort
    Sort Key: t1.a
    ->  Append
@@ -3151,13 +3147,13 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_adv t1 INNER JOIN prt2_adv t2 ON (t1.a =
                ->  Seq Scan on prt2_adv_p1 t2_1
                ->  Hash
                      ->  Seq Scan on prt1_adv_p1 t1_1
-                           Filter: ((a >= 100) AND (a < 300) AND (b = 0))
+                           Filter: (b = 0)
          ->  Hash Join
                Hash Cond: (t2_2.b = t1_2.a)
                ->  Seq Scan on prt2_adv_p2 t2_2
                ->  Hash
                      ->  Seq Scan on prt1_adv_p2 t1_2
-                           Filter: ((a >= 100) AND (a < 300) AND (b = 0))
+                           Filter: (b = 0)
 (15 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_adv t1 INNER JOIN prt2_adv t2 ON (t1.a = t2.b) WHERE t1.a >= 100 AND t1.a < 300 AND t1.b = 0 ORDER BY t1.a, t2.b;
@@ -4456,7 +4452,7 @@ SELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a =
                ->  Seq Scan on plt2_adv_p3 t2_1
                ->  Hash
                      ->  Seq Scan on plt1_adv_p3 t1_1
-                           Filter: ((b < 10) AND (c = ANY ('{0003,0004,0005}'::text[])))
+                           Filter: (b < 10)
          ->  Hash Join
                Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c))
                ->  Seq Scan on plt2_adv_p4 t2_2
@@ -4509,7 +4505,7 @@ SELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 INNER JOIN plt2_adv t2 ON (t1.a =
                ->  Seq Scan on plt2_adv_p3 t2_1
                ->  Hash
                      ->  Seq Scan on plt1_adv_p3 t1_1
-                           Filter: ((b < 10) AND (c = ANY ('{0003,0004,0005}'::text[])))
+                           Filter: (b < 10)
          ->  Hash Join
                Hash Cond: ((t2_2.a = t1_2.a) AND (t2_2.c = t1_2.c))
                ->  Seq Scan on plt2_adv_p4 t2_2
@@ -4699,7 +4695,7 @@ SELECT t1.*, t2.* FROM alpha t1 INNER JOIN beta t2 ON (t1.a = t2.a AND t1.b = t2
          ->  Hash Join
                Hash Cond: ((t1_1.a = t2_1.a) AND (t1_1.b = t2_1.b))
                ->  Seq Scan on alpha_neg_p1 t1_1
-                     Filter: ((b >= 125) AND (b < 225))
+                     Filter: (b >= 125)
                ->  Hash
                      ->  Seq Scan on beta_neg_p1 t2_1
          ->  Hash Join
@@ -4707,7 +4703,7 @@ SELECT t1.*, t2.* FROM alpha t1 INNER JOIN beta t2 ON (t1.a = t2.a AND t1.b = t2
                ->  Seq Scan on beta_neg_p2 t2_2
                ->  Hash
                      ->  Seq Scan on alpha_neg_p2 t1_2
-                           Filter: ((b >= 125) AND (b < 225))
+                           Filter: (b < 225)
          ->  Hash Join
                Hash Cond: ((t2_4.a = t1_4.a) AND (t2_4.b = t1_4.b))
                ->  Append
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 687cf8c5f4..6e5b10e12a 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -27,22 +27,20 @@ explain (costs off) select * from lp where a > 'a' and a < 'd';
 -----------------------------------------------------------
  Append
    ->  Seq Scan on lp_bc lp_1
-         Filter: ((a > 'a'::bpchar) AND (a < 'd'::bpchar))
    ->  Seq Scan on lp_default lp_2
          Filter: ((a > 'a'::bpchar) AND (a < 'd'::bpchar))
-(5 rows)
+(4 rows)
 
 explain (costs off) select * from lp where a > 'a' and a <= 'd';
                          QUERY PLAN                         
 ------------------------------------------------------------
  Append
    ->  Seq Scan on lp_ad lp_1
-         Filter: ((a > 'a'::bpchar) AND (a <= 'd'::bpchar))
+         Filter: (a > 'a'::bpchar)
    ->  Seq Scan on lp_bc lp_2
-         Filter: ((a > 'a'::bpchar) AND (a <= 'd'::bpchar))
    ->  Seq Scan on lp_default lp_3
          Filter: ((a > 'a'::bpchar) AND (a <= 'd'::bpchar))
-(7 rows)
+(6 rows)
 
 explain (costs off) select * from lp where a = 'a';
          QUERY PLAN          
@@ -63,23 +61,17 @@ explain (costs off) select * from lp where a is not null;
 -----------------------------------
  Append
    ->  Seq Scan on lp_ad lp_1
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on lp_bc lp_2
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on lp_ef lp_3
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on lp_g lp_4
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on lp_default lp_5
-         Filter: (a IS NOT NULL)
-(11 rows)
+(6 rows)
 
 explain (costs off) select * from lp where a is null;
        QUERY PLAN       
 ------------------------
  Seq Scan on lp_null lp
-   Filter: (a IS NULL)
-(2 rows)
+(1 row)
 
 explain (costs off) select * from lp where a = 'a' or a = 'c';
                         QUERY PLAN                        
@@ -92,56 +84,44 @@ explain (costs off) select * from lp where a = 'a' or a = 'c';
 (5 rows)
 
 explain (costs off) select * from lp where a is not null and (a = 'a' or a = 'c');
-                                   QUERY PLAN                                   
---------------------------------------------------------------------------------
+                        QUERY PLAN                        
+----------------------------------------------------------
  Append
    ->  Seq Scan on lp_ad lp_1
-         Filter: ((a IS NOT NULL) AND ((a = 'a'::bpchar) OR (a = 'c'::bpchar)))
+         Filter: ((a = 'a'::bpchar) OR (a = 'c'::bpchar))
    ->  Seq Scan on lp_bc lp_2
-         Filter: ((a IS NOT NULL) AND ((a = 'a'::bpchar) OR (a = 'c'::bpchar)))
+         Filter: ((a = 'a'::bpchar) OR (a = 'c'::bpchar))
 (5 rows)
 
 explain (costs off) select * from lp where a <> 'g';
-             QUERY PLAN             
-------------------------------------
+            QUERY PLAN             
+-----------------------------------
  Append
    ->  Seq Scan on lp_ad lp_1
-         Filter: (a <> 'g'::bpchar)
    ->  Seq Scan on lp_bc lp_2
-         Filter: (a <> 'g'::bpchar)
    ->  Seq Scan on lp_ef lp_3
-         Filter: (a <> 'g'::bpchar)
    ->  Seq Scan on lp_default lp_4
-         Filter: (a <> 'g'::bpchar)
-(9 rows)
+(5 rows)
 
 explain (costs off) select * from lp where a <> 'a' and a <> 'd';
-                         QUERY PLAN                          
--------------------------------------------------------------
+            QUERY PLAN             
+-----------------------------------
  Append
    ->  Seq Scan on lp_bc lp_1
-         Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar))
    ->  Seq Scan on lp_ef lp_2
-         Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar))
    ->  Seq Scan on lp_g lp_3
-         Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar))
    ->  Seq Scan on lp_default lp_4
-         Filter: ((a <> 'a'::bpchar) AND (a <> 'd'::bpchar))
-(9 rows)
+(5 rows)
 
 explain (costs off) select * from lp where a not in ('a', 'd');
-                   QUERY PLAN                   
-------------------------------------------------
+            QUERY PLAN             
+-----------------------------------
  Append
    ->  Seq Scan on lp_bc lp_1
-         Filter: (a <> ALL ('{a,d}'::bpchar[]))
    ->  Seq Scan on lp_ef lp_2
-         Filter: (a <> ALL ('{a,d}'::bpchar[]))
    ->  Seq Scan on lp_g lp_3
-         Filter: (a <> ALL ('{a,d}'::bpchar[]))
    ->  Seq Scan on lp_default lp_4
-         Filter: (a <> ALL ('{a,d}'::bpchar[]))
-(9 rows)
+(5 rows)
 
 -- collation matches the partitioning collation, pruning works
 create table coll_pruning (a text collate "C") partition by list (a);
@@ -152,8 +132,7 @@ explain (costs off) select * from coll_pruning where a collate "C" = 'a' collate
                QUERY PLAN                
 -----------------------------------------
  Seq Scan on coll_pruning_a coll_pruning
-   Filter: (a = 'a'::text COLLATE "C")
-(2 rows)
+(1 row)
 
 -- collation doesn't match the partitioning collation, no pruning occurs
 explain (costs off) select * from coll_pruning where a collate "POSIX" = 'a' collate "POSIX";
@@ -193,25 +172,22 @@ explain (costs off) select * from rlp where a < 1;
       QUERY PLAN      
 ----------------------
  Seq Scan on rlp1 rlp
-   Filter: (a < 1)
-(2 rows)
+(1 row)
 
 explain (costs off) select * from rlp where 1 > a;	/* commuted */
       QUERY PLAN      
 ----------------------
  Seq Scan on rlp1 rlp
-   Filter: (1 > a)
-(2 rows)
+(1 row)
 
 explain (costs off) select * from rlp where a <= 1;
           QUERY PLAN          
 ------------------------------
  Append
    ->  Seq Scan on rlp1 rlp_1
-         Filter: (a <= 1)
    ->  Seq Scan on rlp2 rlp_2
          Filter: (a <= 1)
-(5 rows)
+(4 rows)
 
 explain (costs off) select * from rlp where a = 1;
       QUERY PLAN      
@@ -268,65 +244,47 @@ explain (costs off) select * from rlp where a <= 10;
 ---------------------------------------------
  Append
    ->  Seq Scan on rlp1 rlp_1
-         Filter: (a <= 10)
    ->  Seq Scan on rlp2 rlp_2
-         Filter: (a <= 10)
    ->  Seq Scan on rlp_default_10 rlp_3
-         Filter: (a <= 10)
    ->  Seq Scan on rlp_default_default rlp_4
          Filter: (a <= 10)
-(9 rows)
+(6 rows)
 
 explain (costs off) select * from rlp where a > 10;
                   QUERY PLAN                  
 ----------------------------------------------
  Append
    ->  Seq Scan on rlp3abcd rlp_1
-         Filter: (a > 10)
    ->  Seq Scan on rlp3efgh rlp_2
-         Filter: (a > 10)
    ->  Seq Scan on rlp3nullxy rlp_3
-         Filter: (a > 10)
    ->  Seq Scan on rlp3_default rlp_4
-         Filter: (a > 10)
    ->  Seq Scan on rlp4_1 rlp_5
-         Filter: (a > 10)
    ->  Seq Scan on rlp4_2 rlp_6
-         Filter: (a > 10)
    ->  Seq Scan on rlp4_default rlp_7
-         Filter: (a > 10)
    ->  Seq Scan on rlp5_1 rlp_8
-         Filter: (a > 10)
    ->  Seq Scan on rlp5_default rlp_9
-         Filter: (a > 10)
    ->  Seq Scan on rlp_default_30 rlp_10
-         Filter: (a > 10)
    ->  Seq Scan on rlp_default_default rlp_11
          Filter: (a > 10)
-(23 rows)
+(13 rows)
 
 explain (costs off) select * from rlp where a < 15;
                  QUERY PLAN                  
 ---------------------------------------------
  Append
    ->  Seq Scan on rlp1 rlp_1
-         Filter: (a < 15)
    ->  Seq Scan on rlp2 rlp_2
-         Filter: (a < 15)
    ->  Seq Scan on rlp_default_10 rlp_3
-         Filter: (a < 15)
    ->  Seq Scan on rlp_default_default rlp_4
          Filter: (a < 15)
-(9 rows)
+(6 rows)
 
 explain (costs off) select * from rlp where a <= 15;
                  QUERY PLAN                  
 ---------------------------------------------
  Append
    ->  Seq Scan on rlp1 rlp_1
-         Filter: (a <= 15)
    ->  Seq Scan on rlp2 rlp_2
-         Filter: (a <= 15)
    ->  Seq Scan on rlp3abcd rlp_3
          Filter: (a <= 15)
    ->  Seq Scan on rlp3efgh rlp_4
@@ -336,10 +294,9 @@ explain (costs off) select * from rlp where a <= 15;
    ->  Seq Scan on rlp3_default rlp_6
          Filter: (a <= 15)
    ->  Seq Scan on rlp_default_10 rlp_7
-         Filter: (a <= 15)
    ->  Seq Scan on rlp_default_default rlp_8
          Filter: (a <= 15)
-(17 rows)
+(14 rows)
 
 explain (costs off) select * from rlp where a > 15 and b = 'ab';
                        QUERY PLAN                        
@@ -348,17 +305,17 @@ explain (costs off) select * from rlp where a > 15 and b = 'ab';
    ->  Seq Scan on rlp3abcd rlp_1
          Filter: ((a > 15) AND ((b)::text = 'ab'::text))
    ->  Seq Scan on rlp4_1 rlp_2
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: ((b)::text = 'ab'::text)
    ->  Seq Scan on rlp4_2 rlp_3
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: ((b)::text = 'ab'::text)
    ->  Seq Scan on rlp4_default rlp_4
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: ((b)::text = 'ab'::text)
    ->  Seq Scan on rlp5_1 rlp_5
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: ((b)::text = 'ab'::text)
    ->  Seq Scan on rlp5_default rlp_6
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: ((b)::text = 'ab'::text)
    ->  Seq Scan on rlp_default_30 rlp_7
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: ((b)::text = 'ab'::text)
    ->  Seq Scan on rlp_default_default rlp_8
          Filter: ((a > 15) AND ((b)::text = 'ab'::text))
 (17 rows)
@@ -413,106 +370,77 @@ explain (costs off) select * from rlp where a = 16 and b is not null;
 ------------------------------------------------
  Append
    ->  Seq Scan on rlp3abcd rlp_1
-         Filter: ((b IS NOT NULL) AND (a = 16))
+         Filter: (a = 16)
    ->  Seq Scan on rlp3efgh rlp_2
-         Filter: ((b IS NOT NULL) AND (a = 16))
+         Filter: (a = 16)
    ->  Seq Scan on rlp3nullxy rlp_3
          Filter: ((b IS NOT NULL) AND (a = 16))
    ->  Seq Scan on rlp3_default rlp_4
-         Filter: ((b IS NOT NULL) AND (a = 16))
+         Filter: (a = 16)
 (9 rows)
 
 explain (costs off) select * from rlp where a is null;
             QUERY PLAN            
 ----------------------------------
  Seq Scan on rlp_default_null rlp
-   Filter: (a IS NULL)
-(2 rows)
+(1 row)
 
 explain (costs off) select * from rlp where a is not null;
                   QUERY PLAN                  
 ----------------------------------------------
  Append
    ->  Seq Scan on rlp1 rlp_1
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp2 rlp_2
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp3abcd rlp_3
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp3efgh rlp_4
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp3nullxy rlp_5
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp3_default rlp_6
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp4_1 rlp_7
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp4_2 rlp_8
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp4_default rlp_9
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp5_1 rlp_10
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp5_default rlp_11
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp_default_10 rlp_12
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp_default_30 rlp_13
-         Filter: (a IS NOT NULL)
    ->  Seq Scan on rlp_default_default rlp_14
-         Filter: (a IS NOT NULL)
-(29 rows)
+(15 rows)
 
 explain (costs off) select * from rlp where a > 30;
                  QUERY PLAN                  
 ---------------------------------------------
  Append
    ->  Seq Scan on rlp5_1 rlp_1
-         Filter: (a > 30)
    ->  Seq Scan on rlp5_default rlp_2
-         Filter: (a > 30)
    ->  Seq Scan on rlp_default_default rlp_3
          Filter: (a > 30)
-(7 rows)
+(5 rows)
 
 explain (costs off) select * from rlp where a = 30;	/* only default is scanned */
            QUERY PLAN           
 --------------------------------
  Seq Scan on rlp_default_30 rlp
-   Filter: (a = 30)
-(2 rows)
+(1 row)
 
 explain (costs off) select * from rlp where a <= 31;
                   QUERY PLAN                  
 ----------------------------------------------
  Append
    ->  Seq Scan on rlp1 rlp_1
-         Filter: (a <= 31)
    ->  Seq Scan on rlp2 rlp_2
-         Filter: (a <= 31)
    ->  Seq Scan on rlp3abcd rlp_3
-         Filter: (a <= 31)
    ->  Seq Scan on rlp3efgh rlp_4
-         Filter: (a <= 31)
    ->  Seq Scan on rlp3nullxy rlp_5
-         Filter: (a <= 31)
    ->  Seq Scan on rlp3_default rlp_6
-         Filter: (a <= 31)
    ->  Seq Scan on rlp4_1 rlp_7
-         Filter: (a <= 31)
    ->  Seq Scan on rlp4_2 rlp_8
-         Filter: (a <= 31)
    ->  Seq Scan on rlp4_default rlp_9
-         Filter: (a <= 31)
    ->  Seq Scan on rlp5_1 rlp_10
          Filter: (a <= 31)
    ->  Seq Scan on rlp_default_10 rlp_11
-         Filter: (a <= 31)
    ->  Seq Scan on rlp_default_30 rlp_12
-         Filter: (a <= 31)
    ->  Seq Scan on rlp_default_default rlp_13
          Filter: (a <= 31)
-(27 rows)
+(16 rows)
 
 explain (costs off) select * from rlp where a = 1 or a = 7;
            QUERY PLAN           
@@ -552,13 +480,13 @@ explain (costs off) select * from rlp where a = 1 or b = 'ab';
 (25 rows)
 
 explain (costs off) select * from rlp where a > 20 and a < 27;
-               QUERY PLAN                
------------------------------------------
+           QUERY PLAN           
+--------------------------------
  Append
    ->  Seq Scan on rlp4_1 rlp_1
-         Filter: ((a > 20) AND (a < 27))
+         Filter: (a > 20)
    ->  Seq Scan on rlp4_2 rlp_2
-         Filter: ((a > 20) AND (a < 27))
+         Filter: (a < 27)
 (5 rows)
 
 explain (costs off) select * from rlp where a = 29;
@@ -575,24 +503,20 @@ explain (costs off) select * from rlp where a >= 29;
    ->  Seq Scan on rlp4_default rlp_1
          Filter: (a >= 29)
    ->  Seq Scan on rlp5_1 rlp_2
-         Filter: (a >= 29)
    ->  Seq Scan on rlp5_default rlp_3
-         Filter: (a >= 29)
    ->  Seq Scan on rlp_default_30 rlp_4
-         Filter: (a >= 29)
    ->  Seq Scan on rlp_default_default rlp_5
          Filter: (a >= 29)
-(11 rows)
+(8 rows)
 
 explain (costs off) select * from rlp where a < 1 or (a > 20 and a < 25);
                       QUERY PLAN                      
 ------------------------------------------------------
  Append
    ->  Seq Scan on rlp1 rlp_1
-         Filter: ((a < 1) OR ((a > 20) AND (a < 25)))
    ->  Seq Scan on rlp4_1 rlp_2
          Filter: ((a < 1) OR ((a > 20) AND (a < 25)))
-(5 rows)
+(4 rows)
 
 -- where clause contradicts sub-partition's constraint
 explain (costs off) select * from rlp where a = 20 or a = 40;
@@ -614,39 +538,28 @@ explain (costs off) select * from rlp3 where a = 20;   /* empty */
 
 -- redundant clauses are eliminated
 explain (costs off) select * from rlp where a > 1 and a = 10;	/* only default */
-            QUERY PLAN            
-----------------------------------
+           QUERY PLAN           
+--------------------------------
  Seq Scan on rlp_default_10 rlp
-   Filter: ((a > 1) AND (a = 10))
-(2 rows)
+(1 row)
 
 explain (costs off) select * from rlp where a > 1 and a >=15;	/* rlp3 onwards, including default */
                   QUERY PLAN                  
 ----------------------------------------------
  Append
    ->  Seq Scan on rlp3abcd rlp_1
-         Filter: ((a > 1) AND (a >= 15))
    ->  Seq Scan on rlp3efgh rlp_2
-         Filter: ((a > 1) AND (a >= 15))
    ->  Seq Scan on rlp3nullxy rlp_3
-         Filter: ((a > 1) AND (a >= 15))
    ->  Seq Scan on rlp3_default rlp_4
-         Filter: ((a > 1) AND (a >= 15))
    ->  Seq Scan on rlp4_1 rlp_5
-         Filter: ((a > 1) AND (a >= 15))
    ->  Seq Scan on rlp4_2 rlp_6
-         Filter: ((a > 1) AND (a >= 15))
    ->  Seq Scan on rlp4_default rlp_7
-         Filter: ((a > 1) AND (a >= 15))
    ->  Seq Scan on rlp5_1 rlp_8
-         Filter: ((a > 1) AND (a >= 15))
    ->  Seq Scan on rlp5_default rlp_9
-         Filter: ((a > 1) AND (a >= 15))
    ->  Seq Scan on rlp_default_30 rlp_10
-         Filter: ((a > 1) AND (a >= 15))
    ->  Seq Scan on rlp_default_default rlp_11
          Filter: ((a > 1) AND (a >= 15))
-(23 rows)
+(13 rows)
 
 explain (costs off) select * from rlp where a = 1 and a = 3;	/* empty */
         QUERY PLAN        
@@ -733,28 +646,23 @@ explain (costs off) select * from mc3p where a = 10 and abs(b) between 5 and 35;
    ->  Seq Scan on mc3p1 mc3p_1
          Filter: ((a = 10) AND (abs(b) >= 5) AND (abs(b) <= 35))
    ->  Seq Scan on mc3p2 mc3p_2
-         Filter: ((a = 10) AND (abs(b) >= 5) AND (abs(b) <= 35))
    ->  Seq Scan on mc3p3 mc3p_3
-         Filter: ((a = 10) AND (abs(b) >= 5) AND (abs(b) <= 35))
    ->  Seq Scan on mc3p4 mc3p_4
-         Filter: ((a = 10) AND (abs(b) >= 5) AND (abs(b) <= 35))
+         Filter: (abs(b) <= 35)
    ->  Seq Scan on mc3p_default mc3p_5
          Filter: ((a = 10) AND (abs(b) >= 5) AND (abs(b) <= 35))
-(11 rows)
+(9 rows)
 
 explain (costs off) select * from mc3p where a > 10;
               QUERY PLAN               
 ---------------------------------------
  Append
    ->  Seq Scan on mc3p5 mc3p_1
-         Filter: (a > 10)
    ->  Seq Scan on mc3p6 mc3p_2
-         Filter: (a > 10)
    ->  Seq Scan on mc3p7 mc3p_3
-         Filter: (a > 10)
    ->  Seq Scan on mc3p_default mc3p_4
          Filter: (a > 10)
-(9 rows)
+(6 rows)
 
 explain (costs off) select * from mc3p where a >= 10;
               QUERY PLAN               
@@ -763,43 +671,36 @@ explain (costs off) select * from mc3p where a >= 10;
    ->  Seq Scan on mc3p1 mc3p_1
          Filter: (a >= 10)
    ->  Seq Scan on mc3p2 mc3p_2
-         Filter: (a >= 10)
    ->  Seq Scan on mc3p3 mc3p_3
-         Filter: (a >= 10)
    ->  Seq Scan on mc3p4 mc3p_4
-         Filter: (a >= 10)
    ->  Seq Scan on mc3p5 mc3p_5
-         Filter: (a >= 10)
    ->  Seq Scan on mc3p6 mc3p_6
-         Filter: (a >= 10)
    ->  Seq Scan on mc3p7 mc3p_7
-         Filter: (a >= 10)
    ->  Seq Scan on mc3p_default mc3p_8
          Filter: (a >= 10)
-(17 rows)
+(11 rows)
 
 explain (costs off) select * from mc3p where a < 10;
               QUERY PLAN               
 ---------------------------------------
  Append
    ->  Seq Scan on mc3p0 mc3p_1
-         Filter: (a < 10)
    ->  Seq Scan on mc3p1 mc3p_2
          Filter: (a < 10)
    ->  Seq Scan on mc3p_default mc3p_3
          Filter: (a < 10)
-(7 rows)
+(6 rows)
 
 explain (costs off) select * from mc3p where a <= 10 and abs(b) < 10;
                   QUERY PLAN                   
 -----------------------------------------------
  Append
    ->  Seq Scan on mc3p0 mc3p_1
-         Filter: ((a <= 10) AND (abs(b) < 10))
+         Filter: (abs(b) < 10)
    ->  Seq Scan on mc3p1 mc3p_2
-         Filter: ((a <= 10) AND (abs(b) < 10))
+         Filter: (abs(b) < 10)
    ->  Seq Scan on mc3p2 mc3p_3
-         Filter: ((a <= 10) AND (abs(b) < 10))
+         Filter: (abs(b) < 10)
    ->  Seq Scan on mc3p_default mc3p_4
          Filter: ((a <= 10) AND (abs(b) < 10))
 (9 rows)
@@ -812,10 +713,10 @@ explain (costs off) select * from mc3p where a = 11 and abs(b) = 0;
 (2 rows)
 
 explain (costs off) select * from mc3p where a = 20 and abs(b) = 10 and c = 100;
-                      QUERY PLAN                      
-------------------------------------------------------
+               QUERY PLAN                
+-----------------------------------------
  Seq Scan on mc3p6 mc3p
-   Filter: ((a = 20) AND (c = 100) AND (abs(b) = 10))
+   Filter: ((c = 100) AND (abs(b) = 10))
 (2 rows)
 
 explain (costs off) select * from mc3p where a > 20;
@@ -835,12 +736,10 @@ explain (costs off) select * from mc3p where a >= 20;
    ->  Seq Scan on mc3p5 mc3p_1
          Filter: (a >= 20)
    ->  Seq Scan on mc3p6 mc3p_2
-         Filter: (a >= 20)
    ->  Seq Scan on mc3p7 mc3p_3
-         Filter: (a >= 20)
    ->  Seq Scan on mc3p_default mc3p_4
          Filter: (a >= 20)
-(9 rows)
+(7 rows)
 
 explain (costs off) select * from mc3p where (a = 1 and abs(b) = 1 and c = 1) or (a = 10 and abs(b) = 5 and c = 10) or (a > 11 and a < 20);
                                                            QUERY PLAN                                                            
@@ -877,7 +776,6 @@ explain (costs off) select * from mc3p where (a = 1 and abs(b) = 1 and c = 1) or
 -------------------------------------------------------------------------------------------------------------------------------------------------------
  Append
    ->  Seq Scan on mc3p0 mc3p_1
-         Filter: (((a = 1) AND (abs(b) = 1) AND (c = 1)) OR ((a = 10) AND (abs(b) = 5) AND (c = 10)) OR ((a > 11) AND (a < 20)) OR (a < 1) OR (a = 1))
    ->  Seq Scan on mc3p1 mc3p_2
          Filter: (((a = 1) AND (abs(b) = 1) AND (c = 1)) OR ((a = 10) AND (abs(b) = 5) AND (c = 10)) OR ((a > 11) AND (a < 20)) OR (a < 1) OR (a = 1))
    ->  Seq Scan on mc3p2 mc3p_3
@@ -886,7 +784,7 @@ explain (costs off) select * from mc3p where (a = 1 and abs(b) = 1 and c = 1) or
          Filter: (((a = 1) AND (abs(b) = 1) AND (c = 1)) OR ((a = 10) AND (abs(b) = 5) AND (c = 10)) OR ((a > 11) AND (a < 20)) OR (a < 1) OR (a = 1))
    ->  Seq Scan on mc3p_default mc3p_5
          Filter: (((a = 1) AND (abs(b) = 1) AND (c = 1)) OR ((a = 10) AND (abs(b) = 5) AND (c = 10)) OR ((a > 11) AND (a < 20)) OR (a < 1) OR (a = 1))
-(11 rows)
+(10 rows)
 
 explain (costs off) select * from mc3p where a = 1 or abs(b) = 1 or c = 1;
                       QUERY PLAN                      
@@ -923,12 +821,11 @@ explain (costs off) select * from mc3p where (a = 1 and abs(b) = 1) or (a = 10 a
    ->  Seq Scan on mc3p2 mc3p_3
          Filter: (((a = 1) AND (abs(b) = 1)) OR ((a = 10) AND (abs(b) = 10)))
    ->  Seq Scan on mc3p3 mc3p_4
-         Filter: (((a = 1) AND (abs(b) = 1)) OR ((a = 10) AND (abs(b) = 10)))
    ->  Seq Scan on mc3p4 mc3p_5
          Filter: (((a = 1) AND (abs(b) = 1)) OR ((a = 10) AND (abs(b) = 10)))
    ->  Seq Scan on mc3p_default mc3p_6
          Filter: (((a = 1) AND (abs(b) = 1)) OR ((a = 10) AND (abs(b) = 10)))
-(13 rows)
+(12 rows)
 
 explain (costs off) select * from mc3p where (a = 1 and abs(b) = 1) or (a = 10 and abs(b) = 9);
                                  QUERY PLAN                                  
@@ -958,21 +855,17 @@ explain (costs off) select * from mc2p where a < 2;
 ---------------------------------------
  Append
    ->  Seq Scan on mc2p0 mc2p_1
-         Filter: (a < 2)
    ->  Seq Scan on mc2p1 mc2p_2
-         Filter: (a < 2)
    ->  Seq Scan on mc2p2 mc2p_3
-         Filter: (a < 2)
    ->  Seq Scan on mc2p_default mc2p_4
          Filter: (a < 2)
-(9 rows)
+(6 rows)
 
 explain (costs off) select * from mc2p where a = 2 and b < 1;
-           QUERY PLAN            
----------------------------------
+       QUERY PLAN       
+------------------------
  Seq Scan on mc2p3 mc2p
-   Filter: ((b < 1) AND (a = 2))
-(2 rows)
+(1 row)
 
 explain (costs off) select * from mc2p where a > 1;
               QUERY PLAN               
@@ -981,14 +874,11 @@ explain (costs off) select * from mc2p where a > 1;
    ->  Seq Scan on mc2p2 mc2p_1
          Filter: (a > 1)
    ->  Seq Scan on mc2p3 mc2p_2
-         Filter: (a > 1)
    ->  Seq Scan on mc2p4 mc2p_3
-         Filter: (a > 1)
    ->  Seq Scan on mc2p5 mc2p_4
-         Filter: (a > 1)
    ->  Seq Scan on mc2p_default mc2p_5
          Filter: (a > 1)
-(11 rows)
+(8 rows)
 
 explain (costs off) select * from mc2p where a = 1 and b > 1;
            QUERY PLAN            
@@ -1052,15 +942,13 @@ explain (costs off) select * from boolpart where a = false;
            QUERY PLAN            
 ---------------------------------
  Seq Scan on boolpart_f boolpart
-   Filter: (NOT a)
-(2 rows)
+(1 row)
 
 explain (costs off) select * from boolpart where not a = false;
            QUERY PLAN            
 ---------------------------------
  Seq Scan on boolpart_t boolpart
-   Filter: a
-(2 rows)
+(1 row)
 
 explain (costs off) select * from boolpart where a is true or a is not true;
                     QUERY PLAN                    
@@ -1117,10 +1005,10 @@ create table boolrangep_ff1 partition of boolrangep for values from ('false', 'f
 create table boolrangep_ff2 partition of boolrangep for values from ('false', 'false', 50) to ('false', 'false', 100);
 -- try a more complex case that's been known to trip up pruning in the past
 explain (costs off)  select * from boolrangep where not a and not b and c = 25;
-                  QUERY PLAN                  
-----------------------------------------------
+              QUERY PLAN               
+---------------------------------------
  Seq Scan on boolrangep_ff1 boolrangep
-   Filter: ((NOT a) AND (NOT b) AND (c = 25))
+   Filter: (c = 25)
 (2 rows)
 
 -- test scalar-to-array operators
@@ -1189,21 +1077,18 @@ explain (costs off) select * from coercepart where a !~ all ('{ab,bc}');
 (7 rows)
 
 explain (costs off) select * from coercepart where a = any ('{ab,bc}');
-                      QUERY PLAN                       
--------------------------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Append
    ->  Seq Scan on coercepart_ab coercepart_1
-         Filter: ((a)::text = ANY ('{ab,bc}'::text[]))
    ->  Seq Scan on coercepart_bc coercepart_2
-         Filter: ((a)::text = ANY ('{ab,bc}'::text[]))
-(5 rows)
+(3 rows)
 
 explain (costs off) select * from coercepart where a = any ('{ab,null}');
-                    QUERY PLAN                     
----------------------------------------------------
+              QUERY PLAN              
+--------------------------------------
  Seq Scan on coercepart_ab coercepart
-   Filter: ((a)::text = ANY ('{ab,NULL}'::text[]))
-(2 rows)
+(1 row)
 
 explain (costs off) select * from coercepart where a = any (null::text[]);
         QUERY PLAN        
@@ -1213,11 +1098,10 @@ explain (costs off) select * from coercepart where a = any (null::text[]);
 (2 rows)
 
 explain (costs off) select * from coercepart where a = all ('{ab}');
-                  QUERY PLAN                  
-----------------------------------------------
+              QUERY PLAN              
+--------------------------------------
  Seq Scan on coercepart_ab coercepart
-   Filter: ((a)::text = ALL ('{ab}'::text[]))
-(2 rows)
+(1 row)
 
 explain (costs off) select * from coercepart where a = all ('{ab,bc}');
         QUERY PLAN        
@@ -1289,7 +1173,6 @@ explain (costs off) select * from mc2p t1, lateral (select count(*) from mc3p t2
  Nested Loop
    ->  Append
          ->  Seq Scan on mc2p1 t1_1
-               Filter: (a = 1)
          ->  Seq Scan on mc2p2 t1_2
                Filter: (a = 1)
          ->  Seq Scan on mc2p_default t1_3
@@ -1314,7 +1197,7 @@ explain (costs off) select * from mc2p t1, lateral (select count(*) from mc3p t2
                      Filter: ((a = t1.b) AND (c = 1) AND (abs(b) = 1))
                ->  Seq Scan on mc3p_default t2_9
                      Filter: ((a = t1.b) AND (c = 1) AND (abs(b) = 1))
-(28 rows)
+(27 rows)
 
 -- pruning should work fine, because values for a prefix of keys (a, b) are
 -- available
@@ -1324,7 +1207,6 @@ explain (costs off) select * from mc2p t1, lateral (select count(*) from mc3p t2
  Nested Loop
    ->  Append
          ->  Seq Scan on mc2p1 t1_1
-               Filter: (a = 1)
          ->  Seq Scan on mc2p2 t1_2
                Filter: (a = 1)
          ->  Seq Scan on mc2p_default t1_3
@@ -1337,7 +1219,7 @@ explain (costs off) select * from mc2p t1, lateral (select count(*) from mc3p t2
                      Filter: ((c = t1.b) AND (a = 1) AND (abs(b) = 1))
                ->  Seq Scan on mc3p_default t2_3
                      Filter: ((c = t1.b) AND (a = 1) AND (abs(b) = 1))
-(16 rows)
+(15 rows)
 
 -- also here, because values for all keys are provided
 explain (costs off) select * from mc2p t1, lateral (select count(*) from mc3p t2 where t2.a = 1 and abs(t2.b) = 1 and t2.c = 1) s where t1.a = 1;
@@ -1349,12 +1231,11 @@ explain (costs off) select * from mc2p t1, lateral (select count(*) from mc3p t2
                Filter: ((a = 1) AND (c = 1) AND (abs(b) = 1))
    ->  Append
          ->  Seq Scan on mc2p1 t1_1
-               Filter: (a = 1)
          ->  Seq Scan on mc2p2 t1_2
                Filter: (a = 1)
          ->  Seq Scan on mc2p_default t1_3
                Filter: (a = 1)
-(11 rows)
+(10 rows)
 
 --
 -- pruning with clauses containing <> operator
@@ -1369,24 +1250,21 @@ explain (costs off) select * from rp where a <> 1;
 ----------------------------
  Append
    ->  Seq Scan on rp0 rp_1
-         Filter: (a <> 1)
    ->  Seq Scan on rp1 rp_2
          Filter: (a <> 1)
    ->  Seq Scan on rp2 rp_3
-         Filter: (a <> 1)
-(7 rows)
+(5 rows)
 
 explain (costs off) select * from rp where a <> 1 and a <> 2;
-               QUERY PLAN                
------------------------------------------
+         QUERY PLAN         
+----------------------------
  Append
    ->  Seq Scan on rp0 rp_1
-         Filter: ((a <> 1) AND (a <> 2))
    ->  Seq Scan on rp1 rp_2
-         Filter: ((a <> 1) AND (a <> 2))
+         Filter: (a <> 1)
    ->  Seq Scan on rp2 rp_3
-         Filter: ((a <> 1) AND (a <> 2))
-(7 rows)
+         Filter: (a <> 2)
+(6 rows)
 
 -- null partition should be eliminated due to strict <> clause.
 explain (costs off) select * from lp where a <> 'a';
@@ -1396,14 +1274,10 @@ explain (costs off) select * from lp where a <> 'a';
    ->  Seq Scan on lp_ad lp_1
          Filter: (a <> 'a'::bpchar)
    ->  Seq Scan on lp_bc lp_2
-         Filter: (a <> 'a'::bpchar)
    ->  Seq Scan on lp_ef lp_3
-         Filter: (a <> 'a'::bpchar)
    ->  Seq Scan on lp_g lp_4
-         Filter: (a <> 'a'::bpchar)
    ->  Seq Scan on lp_default lp_5
-         Filter: (a <> 'a'::bpchar)
-(11 rows)
+(7 rows)
 
 -- ensure we detect contradictions in clauses; a can't be NULL and NOT NULL.
 explain (costs off) select * from lp where a <> 'a' and a is null;
@@ -1414,32 +1288,27 @@ explain (costs off) select * from lp where a <> 'a' and a is null;
 (2 rows)
 
 explain (costs off) select * from lp where (a <> 'a' and a <> 'd') or a is null;
-                                  QUERY PLAN                                  
-------------------------------------------------------------------------------
+            QUERY PLAN             
+-----------------------------------
  Append
    ->  Seq Scan on lp_bc lp_1
-         Filter: (((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) OR (a IS NULL))
    ->  Seq Scan on lp_ef lp_2
-         Filter: (((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) OR (a IS NULL))
    ->  Seq Scan on lp_g lp_3
-         Filter: (((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) OR (a IS NULL))
    ->  Seq Scan on lp_null lp_4
-         Filter: (((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) OR (a IS NULL))
    ->  Seq Scan on lp_default lp_5
-         Filter: (((a <> 'a'::bpchar) AND (a <> 'd'::bpchar)) OR (a IS NULL))
-(11 rows)
+(6 rows)
 
 -- check that it also works for a partitioned table that's not root,
 -- which in this case are partitions of rlp that are themselves
 -- list-partitioned on b
 explain (costs off) select * from rlp where a = 15 and b <> 'ab' and b <> 'cd' and b <> 'xy' and b is not null;
-                                                                QUERY PLAN                                                                
-------------------------------------------------------------------------------------------------------------------------------------------
+              QUERY PLAN              
+--------------------------------------
  Append
    ->  Seq Scan on rlp3efgh rlp_1
-         Filter: ((b IS NOT NULL) AND ((b)::text <> 'ab'::text) AND ((b)::text <> 'cd'::text) AND ((b)::text <> 'xy'::text) AND (a = 15))
+         Filter: (a = 15)
    ->  Seq Scan on rlp3_default rlp_2
-         Filter: ((b IS NOT NULL) AND ((b)::text <> 'ab'::text) AND ((b)::text <> 'cd'::text) AND ((b)::text <> 'xy'::text) AND (a = 15))
+         Filter: (a = 15)
 (5 rows)
 
 --
@@ -1780,9 +1649,9 @@ explain (analyze, costs off, summary off, timing off) execute ab_q1 (2, 2);
  Append (actual rows=0 loops=1)
    Subplans Removed: 4
    ->  Seq Scan on ab_a2_b1 ab_1 (actual rows=0 loops=1)
-         Filter: ((a >= $1) AND (a <= $2) AND (b < 3))
+         Filter: ((a >= $1) AND (a <= $2))
    ->  Seq Scan on ab_a2_b2 ab_2 (actual rows=0 loops=1)
-         Filter: ((a >= $1) AND (a <= $2) AND (b < 3))
+         Filter: ((a >= $1) AND (a <= $2))
 (6 rows)
 
 explain (analyze, costs off, summary off, timing off) execute ab_q1 (2, 4);
@@ -1791,13 +1660,13 @@ explain (analyze, costs off, summary off, timing off) execute ab_q1 (2, 4);
  Append (actual rows=0 loops=1)
    Subplans Removed: 2
    ->  Seq Scan on ab_a2_b1 ab_1 (actual rows=0 loops=1)
-         Filter: ((a >= $1) AND (a <= $2) AND (b < 3))
+         Filter: ((a >= $1) AND (a <= $2))
    ->  Seq Scan on ab_a2_b2 ab_2 (actual rows=0 loops=1)
-         Filter: ((a >= $1) AND (a <= $2) AND (b < 3))
+         Filter: ((a >= $1) AND (a <= $2))
    ->  Seq Scan on ab_a3_b1 ab_3 (actual rows=0 loops=1)
-         Filter: ((a >= $1) AND (a <= $2) AND (b < 3))
+         Filter: ((a >= $1) AND (a <= $2))
    ->  Seq Scan on ab_a3_b2 ab_4 (actual rows=0 loops=1)
-         Filter: ((a >= $1) AND (a <= $2) AND (b < 3))
+         Filter: ((a >= $1) AND (a <= $2))
 (10 rows)
 
 -- Ensure a mix of PARAM_EXTERN and PARAM_EXEC Params work together at
@@ -1952,11 +1821,11 @@ select explain_parallel_append('execute ab_q4 (2, 2)');
                ->  Parallel Append (actual rows=N loops=N)
                      Subplans Removed: 6
                      ->  Parallel Seq Scan on ab_a2_b1 ab_1 (actual rows=N loops=N)
-                           Filter: ((a >= $1) AND (a <= $2) AND (b < 4))
+                           Filter: ((a >= $1) AND (a <= $2))
                      ->  Parallel Seq Scan on ab_a2_b2 ab_2 (actual rows=N loops=N)
-                           Filter: ((a >= $1) AND (a <= $2) AND (b < 4))
+                           Filter: ((a >= $1) AND (a <= $2))
                      ->  Parallel Seq Scan on ab_a2_b3 ab_3 (actual rows=N loops=N)
-                           Filter: ((a >= $1) AND (a <= $2) AND (b < 4))
+                           Filter: ((a >= $1) AND (a <= $2))
 (13 rows)
 
 -- Test run-time pruning with IN lists.
@@ -1973,11 +1842,11 @@ select explain_parallel_append('execute ab_q5 (1, 1, 1)');
                ->  Parallel Append (actual rows=N loops=N)
                      Subplans Removed: 6
                      ->  Parallel Seq Scan on ab_a1_b1 ab_1 (actual rows=N loops=N)
-                           Filter: ((b < 4) AND (a = ANY (ARRAY[$1, $2, $3])))
+                           Filter: (a = ANY (ARRAY[$1, $2, $3]))
                      ->  Parallel Seq Scan on ab_a1_b2 ab_2 (actual rows=N loops=N)
-                           Filter: ((b < 4) AND (a = ANY (ARRAY[$1, $2, $3])))
+                           Filter: (a = ANY (ARRAY[$1, $2, $3]))
                      ->  Parallel Seq Scan on ab_a1_b3 ab_3 (actual rows=N loops=N)
-                           Filter: ((b < 4) AND (a = ANY (ARRAY[$1, $2, $3])))
+                           Filter: (a = ANY (ARRAY[$1, $2, $3]))
 (13 rows)
 
 select explain_parallel_append('execute ab_q5 (2, 3, 3)');
@@ -1991,17 +1860,17 @@ select explain_parallel_append('execute ab_q5 (2, 3, 3)');
                ->  Parallel Append (actual rows=N loops=N)
                      Subplans Removed: 3
                      ->  Parallel Seq Scan on ab_a2_b1 ab_1 (actual rows=N loops=N)
-                           Filter: ((b < 4) AND (a = ANY (ARRAY[$1, $2, $3])))
+                           Filter: (a = ANY (ARRAY[$1, $2, $3]))
                      ->  Parallel Seq Scan on ab_a2_b2 ab_2 (actual rows=N loops=N)
-                           Filter: ((b < 4) AND (a = ANY (ARRAY[$1, $2, $3])))
+                           Filter: (a = ANY (ARRAY[$1, $2, $3]))
                      ->  Parallel Seq Scan on ab_a2_b3 ab_3 (actual rows=N loops=N)
-                           Filter: ((b < 4) AND (a = ANY (ARRAY[$1, $2, $3])))
+                           Filter: (a = ANY (ARRAY[$1, $2, $3]))
                      ->  Parallel Seq Scan on ab_a3_b1 ab_4 (actual rows=N loops=N)
-                           Filter: ((b < 4) AND (a = ANY (ARRAY[$1, $2, $3])))
+                           Filter: (a = ANY (ARRAY[$1, $2, $3]))
                      ->  Parallel Seq Scan on ab_a3_b2 ab_5 (actual rows=N loops=N)
-                           Filter: ((b < 4) AND (a = ANY (ARRAY[$1, $2, $3])))
+                           Filter: (a = ANY (ARRAY[$1, $2, $3]))
                      ->  Parallel Seq Scan on ab_a3_b3 ab_6 (actual rows=N loops=N)
-                           Filter: ((b < 4) AND (a = ANY (ARRAY[$1, $2, $3])))
+                           Filter: (a = ANY (ARRAY[$1, $2, $3]))
 (19 rows)
 
 -- Try some params whose values do not belong to any partition.
@@ -2019,9 +1888,9 @@ select explain_parallel_append('execute ab_q5 (33, 44, 55)');
 
 -- Test Parallel Append with PARAM_EXEC Params
 select explain_parallel_append('select count(*) from ab where (a = (select 1) or a = (select 3)) and b = 2');
-                           explain_parallel_append                            
-------------------------------------------------------------------------------
- Aggregate (actual rows=N loops=N)
+                              explain_parallel_append                               
+------------------------------------------------------------------------------------
+ Finalize Aggregate (actual rows=N loops=N)
    InitPlan 1 (returns $0)
      ->  Result (actual rows=N loops=N)
    InitPlan 2 (returns $1)
@@ -2030,14 +1899,15 @@ select explain_parallel_append('select count(*) from ab where (a = (select 1) or
          Workers Planned: 2
          Params Evaluated: $0, $1
          Workers Launched: N
-         ->  Parallel Append (actual rows=N loops=N)
-               ->  Parallel Seq Scan on ab_a1_b2 ab_1 (actual rows=N loops=N)
-                     Filter: ((b = 2) AND ((a = $0) OR (a = $1)))
-               ->  Parallel Seq Scan on ab_a2_b2 ab_2 (never executed)
-                     Filter: ((b = 2) AND ((a = $0) OR (a = $1)))
-               ->  Parallel Seq Scan on ab_a3_b2 ab_3 (actual rows=N loops=N)
-                     Filter: ((b = 2) AND ((a = $0) OR (a = $1)))
-(16 rows)
+         ->  Partial Aggregate (actual rows=N loops=N)
+               ->  Parallel Append (actual rows=N loops=N)
+                     ->  Parallel Seq Scan on ab_a1_b2 ab_1 (actual rows=N loops=N)
+                           Filter: ((a = $0) OR (a = $1))
+                     ->  Parallel Seq Scan on ab_a2_b2 ab_2 (never executed)
+                           Filter: ((a = $0) OR (a = $1))
+                     ->  Parallel Seq Scan on ab_a3_b2 ab_3 (actual rows=N loops=N)
+                           Filter: ((a = $0) OR (a = $1))
+(17 rows)
 
 -- Test pruning during parallel nested loop query
 create table lprt_a (a int not null);
@@ -2291,27 +2161,18 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1
 -- Test run-time partition pruning with UNION ALL parents
 explain (analyze, costs off, summary off, timing off)
 select * from (select * from ab where a = 1 union all select * from ab) ab where b = (select 1);
-                                  QUERY PLAN                                   
--------------------------------------------------------------------------------
+                           QUERY PLAN                           
+----------------------------------------------------------------
  Append (actual rows=0 loops=1)
    InitPlan 1 (returns $0)
      ->  Result (actual rows=1 loops=1)
    ->  Append (actual rows=0 loops=1)
-         ->  Bitmap Heap Scan on ab_a1_b1 ab_11 (actual rows=0 loops=1)
-               Recheck Cond: (a = 1)
+         ->  Seq Scan on ab_a1_b1 ab_11 (actual rows=0 loops=1)
                Filter: (b = $0)
-               ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
-                     Index Cond: (a = 1)
-         ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
-               Recheck Cond: (a = 1)
+         ->  Seq Scan on ab_a1_b2 ab_12 (never executed)
                Filter: (b = $0)
-               ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
-                     Index Cond: (a = 1)
-         ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
-               Recheck Cond: (a = 1)
+         ->  Seq Scan on ab_a1_b3 ab_13 (never executed)
                Filter: (b = $0)
-               ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
-                     Index Cond: (a = 1)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
          Filter: (b = $0)
    ->  Seq Scan on ab_a1_b2 ab_2 (never executed)
@@ -2330,32 +2191,23 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where
          Filter: (b = $0)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = $0)
-(37 rows)
+(28 rows)
 
 -- A case containing a UNION ALL with a non-partitioned child.
 explain (analyze, costs off, summary off, timing off)
 select * from (select * from ab where a = 1 union all (values(10,5)) union all select * from ab) ab where b = (select 1);
-                                  QUERY PLAN                                   
--------------------------------------------------------------------------------
+                           QUERY PLAN                           
+----------------------------------------------------------------
  Append (actual rows=0 loops=1)
    InitPlan 1 (returns $0)
      ->  Result (actual rows=1 loops=1)
    ->  Append (actual rows=0 loops=1)
-         ->  Bitmap Heap Scan on ab_a1_b1 ab_11 (actual rows=0 loops=1)
-               Recheck Cond: (a = 1)
+         ->  Seq Scan on ab_a1_b1 ab_11 (actual rows=0 loops=1)
                Filter: (b = $0)
-               ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
-                     Index Cond: (a = 1)
-         ->  Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed)
-               Recheck Cond: (a = 1)
+         ->  Seq Scan on ab_a1_b2 ab_12 (never executed)
                Filter: (b = $0)
-               ->  Bitmap Index Scan on ab_a1_b2_a_idx (never executed)
-                     Index Cond: (a = 1)
-         ->  Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed)
-               Recheck Cond: (a = 1)
+         ->  Seq Scan on ab_a1_b3 ab_13 (never executed)
                Filter: (b = $0)
-               ->  Bitmap Index Scan on ab_a1_b3_a_idx (never executed)
-                     Index Cond: (a = 1)
    ->  Result (actual rows=0 loops=1)
          One-Time Filter: (5 = $0)
    ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
@@ -2376,7 +2228,7 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s
          Filter: (b = $0)
    ->  Seq Scan on ab_a3_b3 ab_9 (never executed)
          Filter: (b = $0)
-(39 rows)
+(30 rows)
 
 -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning.
 create table xy_1 (x int, y int);
@@ -2435,74 +2287,34 @@ deallocate ab_q6;
 insert into ab values (1,2);
 explain (analyze, costs off, summary off, timing off)
 update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;
-                                     QUERY PLAN                                      
--------------------------------------------------------------------------------------
+                               QUERY PLAN                               
+------------------------------------------------------------------------
  Update on ab_a1 (actual rows=0 loops=1)
    Update on ab_a1_b1 ab_a1_1
    Update on ab_a1_b2 ab_a1_2
    Update on ab_a1_b3 ab_a1_3
    ->  Nested Loop (actual rows=0 loops=1)
          ->  Append (actual rows=1 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
-                     Recheck Cond: (a = 1)
-                     Heap Blocks: exact=1
-                     ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
+               ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+               ->  Seq Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
+               ->  Seq Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
          ->  Materialize (actual rows=0 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b1 ab_a1_1 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
+               ->  Seq Scan on ab_a1_b1 ab_a1_1 (actual rows=0 loops=1)
    ->  Nested Loop (actual rows=1 loops=1)
          ->  Append (actual rows=1 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
-                     Recheck Cond: (a = 1)
-                     Heap Blocks: exact=1
-                     ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
+               ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+               ->  Seq Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
+               ->  Seq Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
          ->  Materialize (actual rows=1 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
-                     Recheck Cond: (a = 1)
-                     Heap Blocks: exact=1
-                     ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
+               ->  Seq Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
    ->  Nested Loop (actual rows=0 loops=1)
          ->  Append (actual rows=1 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
-                     Recheck Cond: (a = 1)
-                     Heap Blocks: exact=1
-                     ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
+               ->  Seq Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+               ->  Seq Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
+               ->  Seq Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
          ->  Materialize (actual rows=0 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-(65 rows)
+               ->  Seq Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
+(25 rows)
 
 table ab;
  a | b 
@@ -3013,9 +2825,9 @@ select * from mc3p where a < 3 and abs(b) = 1;
 --------------------------------------------------------
  Append (actual rows=3 loops=1)
    ->  Seq Scan on mc3p0 mc3p_1 (actual rows=1 loops=1)
-         Filter: ((a < 3) AND (abs(b) = 1))
+         Filter: (abs(b) = 1)
    ->  Seq Scan on mc3p1 mc3p_2 (actual rows=1 loops=1)
-         Filter: ((a < 3) AND (abs(b) = 1))
+         Filter: (abs(b) = 1)
    ->  Seq Scan on mc3p2 mc3p_3 (actual rows=1 loops=1)
          Filter: ((a < 3) AND (abs(b) = 1))
 (7 rows)
@@ -3210,8 +3022,7 @@ explain (costs off) select * from pp_arrpart where a = '{1}';
              QUERY PLAN             
 ------------------------------------
  Seq Scan on pp_arrpart1 pp_arrpart
-   Filter: (a = '{1}'::integer[])
-(2 rows)
+(1 row)
 
 explain (costs off) select * from pp_arrpart where a = '{1, 2}';
         QUERY PLAN        
@@ -3225,10 +3036,9 @@ explain (costs off) select * from pp_arrpart where a in ('{4, 5}', '{1}');
 ----------------------------------------------------------------------
  Append
    ->  Seq Scan on pp_arrpart1 pp_arrpart_1
-         Filter: ((a = '{4,5}'::integer[]) OR (a = '{1}'::integer[]))
    ->  Seq Scan on pp_arrpart2 pp_arrpart_2
          Filter: ((a = '{4,5}'::integer[]) OR (a = '{1}'::integer[]))
-(5 rows)
+(4 rows)
 
 explain (costs off) update pp_arrpart set a = a where a = '{1}';
                  QUERY PLAN                 
@@ -3236,8 +3046,7 @@ explain (costs off) update pp_arrpart set a = a where a = '{1}';
  Update on pp_arrpart
    Update on pp_arrpart1 pp_arrpart_1
    ->  Seq Scan on pp_arrpart1 pp_arrpart_1
-         Filter: (a = '{1}'::integer[])
-(4 rows)
+(3 rows)
 
 explain (costs off) delete from pp_arrpart where a = '{1}';
                  QUERY PLAN                 
@@ -3245,8 +3054,7 @@ explain (costs off) delete from pp_arrpart where a = '{1}';
  Delete on pp_arrpart
    Delete on pp_arrpart1 pp_arrpart_1
    ->  Seq Scan on pp_arrpart1 pp_arrpart_1
-         Filter: (a = '{1}'::integer[])
-(4 rows)
+(3 rows)
 
 drop table pp_arrpart;
 -- array type hash partition key
@@ -3296,8 +3104,7 @@ explain (costs off) select * from pp_enumpart where a = 'blue';
                 QUERY PLAN                
 ------------------------------------------
  Seq Scan on pp_enumpart_blue pp_enumpart
-   Filter: (a = 'blue'::pp_colors)
-(2 rows)
+(1 row)
 
 explain (costs off) select * from pp_enumpart where a = 'black';
         QUERY PLAN        
@@ -3317,8 +3124,7 @@ explain (costs off) select * from pp_recpart where a = '(1,1)'::pp_rectype;
               QUERY PLAN              
 --------------------------------------
  Seq Scan on pp_recpart_11 pp_recpart
-   Filter: (a = '(1,1)'::pp_rectype)
-(2 rows)
+(1 row)
 
 explain (costs off) select * from pp_recpart where a = '(1,2)'::pp_rectype;
         QUERY PLAN        
@@ -3337,8 +3143,7 @@ explain (costs off) select * from pp_intrangepart where a = '[1,2]'::int4range;
                   QUERY PLAN                   
 -----------------------------------------------
  Seq Scan on pp_intrangepart12 pp_intrangepart
-   Filter: (a = '[1,3)'::int4range)
-(2 rows)
+(1 row)
 
 explain (costs off) select * from pp_intrangepart where a = '(1,2)'::int4range;
         QUERY PLAN        
@@ -3358,8 +3163,7 @@ explain (costs off) select * from pp_lp where a = 1;
         QUERY PLAN        
 --------------------------
  Seq Scan on pp_lp1 pp_lp
-   Filter: (a = 1)
-(2 rows)
+(1 row)
 
 explain (costs off) update pp_lp set value = 10 where a = 1;
             QUERY PLAN            
@@ -3367,8 +3171,7 @@ explain (costs off) update pp_lp set value = 10 where a = 1;
  Update on pp_lp
    Update on pp_lp1 pp_lp_1
    ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
-(4 rows)
+(3 rows)
 
 explain (costs off) delete from pp_lp where a = 1;
             QUERY PLAN            
@@ -3376,8 +3179,7 @@ explain (costs off) delete from pp_lp where a = 1;
  Delete on pp_lp
    Delete on pp_lp1 pp_lp_1
    ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
-(4 rows)
+(3 rows)
 
 set enable_partition_pruning = off;
 set constraint_exclusion = 'partition'; -- this should not affect the result.
@@ -3386,10 +3188,9 @@ explain (costs off) select * from pp_lp where a = 1;
 ----------------------------------
  Append
    ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
    ->  Seq Scan on pp_lp2 pp_lp_2
          Filter: (a = 1)
-(5 rows)
+(4 rows)
 
 explain (costs off) update pp_lp set value = 10 where a = 1;
             QUERY PLAN            
@@ -3398,10 +3199,9 @@ explain (costs off) update pp_lp set value = 10 where a = 1;
    Update on pp_lp1 pp_lp_1
    Update on pp_lp2 pp_lp_2
    ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
    ->  Seq Scan on pp_lp2 pp_lp_2
          Filter: (a = 1)
-(7 rows)
+(6 rows)
 
 explain (costs off) delete from pp_lp where a = 1;
             QUERY PLAN            
@@ -3410,10 +3210,9 @@ explain (costs off) delete from pp_lp where a = 1;
    Delete on pp_lp1 pp_lp_1
    Delete on pp_lp2 pp_lp_2
    ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
    ->  Seq Scan on pp_lp2 pp_lp_2
          Filter: (a = 1)
-(7 rows)
+(6 rows)
 
 set constraint_exclusion = 'off'; -- this should not affect the result.
 explain (costs off) select * from pp_lp where a = 1;
@@ -3421,10 +3220,9 @@ explain (costs off) select * from pp_lp where a = 1;
 ----------------------------------
  Append
    ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
    ->  Seq Scan on pp_lp2 pp_lp_2
          Filter: (a = 1)
-(5 rows)
+(4 rows)
 
 explain (costs off) update pp_lp set value = 10 where a = 1;
             QUERY PLAN            
@@ -3433,10 +3231,9 @@ explain (costs off) update pp_lp set value = 10 where a = 1;
    Update on pp_lp1 pp_lp_1
    Update on pp_lp2 pp_lp_2
    ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
    ->  Seq Scan on pp_lp2 pp_lp_2
          Filter: (a = 1)
-(7 rows)
+(6 rows)
 
 explain (costs off) delete from pp_lp where a = 1;
             QUERY PLAN            
@@ -3445,10 +3242,9 @@ explain (costs off) delete from pp_lp where a = 1;
    Delete on pp_lp1 pp_lp_1
    Delete on pp_lp2 pp_lp_2
    ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
    ->  Seq Scan on pp_lp2 pp_lp_2
          Filter: (a = 1)
-(7 rows)
+(6 rows)
 
 drop table pp_lp;
 -- Ensure enable_partition_prune does not affect non-partitioned tables.
@@ -3468,8 +3264,7 @@ explain (costs off) select * from inh_lp where a = 1;
    ->  Seq Scan on inh_lp inh_lp_1
          Filter: (a = 1)
    ->  Seq Scan on inh_lp1 inh_lp_2
-         Filter: (a = 1)
-(5 rows)
+(4 rows)
 
 explain (costs off) update inh_lp set value = 10 where a = 1;
              QUERY PLAN             
@@ -3480,8 +3275,7 @@ explain (costs off) update inh_lp set value = 10 where a = 1;
    ->  Seq Scan on inh_lp
          Filter: (a = 1)
    ->  Seq Scan on inh_lp1 inh_lp_1
-         Filter: (a = 1)
-(7 rows)
+(6 rows)
 
 explain (costs off) delete from inh_lp where a = 1;
              QUERY PLAN             
@@ -3492,8 +3286,7 @@ explain (costs off) delete from inh_lp where a = 1;
    ->  Seq Scan on inh_lp
          Filter: (a = 1)
    ->  Seq Scan on inh_lp1 inh_lp_1
-         Filter: (a = 1)
-(7 rows)
+(6 rows)
 
 -- Ensure we don't exclude normal relations when we only expect to exclude
 -- inheritance children
@@ -3553,15 +3346,15 @@ from (
       select 1, 1, 1
      ) s(a, b, c)
 where s.a = 1 and s.b = 1 and s.c = (select 1);
-                     QUERY PLAN                     
-----------------------------------------------------
+               QUERY PLAN               
+----------------------------------------
  Append
    InitPlan 1 (returns $0)
      ->  Result
    ->  Seq Scan on p1 p
-         Filter: ((a = 1) AND (b = 1) AND (c = $0))
+         Filter: ((b = 1) AND (c = $0))
    ->  Seq Scan on q111 q1
-         Filter: ((a = 1) AND (b = 1) AND (c = $0))
+         Filter: (c = $0)
    ->  Result
          One-Time Filter: (1 = $0)
 (9 rows)
@@ -3681,10 +3474,10 @@ create table rp_prefix_test1_p2 partition of rp_prefix_test1 for values from (2,
 -- Don't call get_steps_using_prefix() with the last partition key b plus
 -- an empty prefix
 explain (costs off) select * from rp_prefix_test1 where a <= 1 and b = 'a';
-                    QUERY PLAN                    
---------------------------------------------------
+                   QUERY PLAN                   
+------------------------------------------------
  Seq Scan on rp_prefix_test1_p1 rp_prefix_test1
-   Filter: ((a <= 1) AND ((b)::text = 'a'::text))
+   Filter: ((b)::text = 'a'::text)
 (2 rows)
 
 create table rp_prefix_test2 (a int, b int, c int) partition by range(a, b, c);
@@ -3696,8 +3489,7 @@ explain (costs off) select * from rp_prefix_test2 where a <= 1 and b = 1 and c >
                    QUERY PLAN                   
 ------------------------------------------------
  Seq Scan on rp_prefix_test2_p1 rp_prefix_test2
-   Filter: ((a <= 1) AND (c >= 0) AND (b = 1))
-(2 rows)
+(1 row)
 
 create table rp_prefix_test3 (a int, b int, c int, d int) partition by range(a, b, c, d);
 create table rp_prefix_test3_p1 partition of rp_prefix_test3 for values from (1, 1, 1, 0) to (1, 1, 1, 10);
@@ -3705,11 +3497,10 @@ create table rp_prefix_test3_p2 partition of rp_prefix_test3 for values from (2,
 -- Test that get_steps_using_prefix() handles a prefix that contains multiple
 -- clauses for the partition key b (ie, b >= 1 and b >= 2)
 explain (costs off) select * from rp_prefix_test3 where a >= 1 and b >= 1 and b >= 2 and c >= 2 and d >= 0;
-                                QUERY PLAN                                
---------------------------------------------------------------------------
+                   QUERY PLAN                   
+------------------------------------------------
  Seq Scan on rp_prefix_test3_p2 rp_prefix_test3
-   Filter: ((a >= 1) AND (b >= 1) AND (b >= 2) AND (c >= 2) AND (d >= 0))
-(2 rows)
+(1 row)
 
 create table hp_prefix_test (a int, b int, c int, d int) partition by hash (a part_test_int4_ops, b part_test_int4_ops, c part_test_int4_ops, d part_test_int4_ops);
 create table hp_prefix_test_p1 partition of hp_prefix_test for values with (modulus 2, remainder 0);
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 9506aaef82..3309d26c87 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -1057,10 +1057,10 @@ NOTICE:  f_leak => awesome science fiction
 (4 rows)
 
 EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle);
-                          QUERY PLAN                          
---------------------------------------------------------------
+                     QUERY PLAN                     
+----------------------------------------------------
  Seq Scan on part_document_fiction part_document
-   Filter: ((cid < 55) AND (dlevel <= $0) AND f_leak(dtitle))
+   Filter: ((dlevel <= $0) AND f_leak(dtitle))
    InitPlan 1 (returns $0)
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
@@ -1135,10 +1135,10 @@ NOTICE:  f_leak => awesome science fiction
 (4 rows)
 
 EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle);
-                          QUERY PLAN                          
---------------------------------------------------------------
+                     QUERY PLAN                     
+----------------------------------------------------
  Seq Scan on part_document_fiction part_document
-   Filter: ((cid < 55) AND (dlevel <= $0) AND f_leak(dtitle))
+   Filter: ((dlevel <= $0) AND f_leak(dtitle))
    InitPlan 1 (returns $0)
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
diff --git a/src/test/regress/expected/update.out b/src/test/regress/expected/update.out
index bf939d79f6..0d821ade5b 100644
--- a/src/test/regress/expected/update.out
+++ b/src/test/regress/expected/update.out
@@ -327,12 +327,10 @@ EXPLAIN (costs off) UPDATE range_parted set c = c - 50 WHERE c > 97;
    ->  Seq Scan on part_c_1_100 range_parted_4
          Filter: (c > '97'::numeric)
    ->  Seq Scan on part_d_1_15 range_parted_5
-         Filter: (c > '97'::numeric)
    ->  Seq Scan on part_d_15_20 range_parted_6
-         Filter: (c > '97'::numeric)
    ->  Seq Scan on part_b_20_b_30 range_parted_7
          Filter: (c > '97'::numeric)
-(22 rows)
+(20 rows)
 
 -- fail, row movement happens only within the partition subtree.
 UPDATE part_c_100_200 set c = c - 20, d = c WHERE c = 105;
-- 
2.17.0


--X1bOJ3K7DJ5YkBrT
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
 filename="v3-0002-Avoid-bitmap-index-scan-inconsistent-with-partiti.patch"



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-15 13:37  Akshay Joshi <[email protected]>
  0 siblings, 2 replies; 44+ messages in thread

From: Akshay Joshi @ 2025-10-15 13:37 UTC (permalink / raw)
  To: pgsql-hackers

Hi Hackers,

I’m submitting a patch as part of the broader Retail DDL Functions project
described by Andrew Dunstan
https://www.postgresql.org/message-id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9%40dunslane.net

This patch adds a new system function pg_get_policy_ddl(table, policy_name,
pretty), which reconstructs the CREATE POLICY statement for a given table
and policy. When the pretty flag is set to true, the function returns a
neatly formatted, multi-line DDL statement instead of a single-line
statement.

Usage examples:

1) SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);  -- *non-pretty
formatted DDL*
                                               pg_get_policy_ddl

---------------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE POLICY rls_p8 ON rls_tbl_1 AS PERMISSIVE FOR ALL TO
regress_rls_alice, regress_rls_dave USING (true);

2) SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);   -- *pretty
formatted DDL*
               pg_get_policy_ddl
------------------------------------------------
 CREATE POLICY rls_p8 ON rls_tbl_1
         AS PERMISSIVE
         FOR ALL
         TO regress_rls_alice, regress_rls_dave
         USING (true)
 ;

The patch includes documentation, in-code comments, and regression tests,
all of which pass successfully.

-----
Regards,
Akshay Joshi
EDB (EnterpriseDB)


Attachments:

  [application/octet-stream] 0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch (22.3K, ../../CANxoLDdJsRJqnjMXV3yjsk07Z5iRWxG-c2hZJC7bAKqf8ZXj_A@mail.gmail.com/3-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From 7445dc629c2139905f73a6128d4ed30597bdecb1 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 10 Oct 2025 15:46:13 +0530
Subject: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY
 statements

This patch introduces a new system function:
pg_get_policy_ddl(regclass table, name policy_name, bool pretty),
which reconstructs the CREATE POLICY statement for the specified policy.

Usage examples:
SELECT pg_get_policy_ddl('rls_table', 'pol1', false); -- non-pretty formatted DDL
SELECT pg_get_policy_ddl('rls_table', 'pol1', true);  -- pretty formatted DDL

Reference: PG-163
Author: Akshay Joshi [email protected]
---
 doc/src/sgml/func/func-info.sgml          |  45 +++++
 src/backend/commands/policy.c             |  27 +++
 src/backend/utils/adt/ruleutils.c         | 178 +++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   3 +
 src/include/commands/policy.h             |   2 +
 src/test/regress/expected/rowsecurity.out | 199 +++++++++++++++++++++-
 src/test/regress/sql/rowsecurity.sql      |  78 +++++++++
 7 files changed, 531 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c..4b9c661c20b 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </para>
+
+   <table id="functions-get-object-ddl-table">
+    <title>Get Object DDL Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>, <parameter>policy_name</parameter> <type>name</type>, <parameter>pretty</parameter> <type>boolean</type> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the CREATE POLICY statement from the system catalogs for a specified table and policy name.
+        When the pretty flag is set to true, the function returns a well-formatted DDL statement.
+        The result is a comprehensive <command>CREATE POLICY</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 83056960fe4..1abe0c44353 100644
--- a/src/backend/commands/policy.c
+++ b/src/backend/commands/policy.c
@@ -128,6 +128,33 @@ parse_policy_command(const char *cmd_name)
 	return polcmd;
 }
 
+/*
+ * get_policy_cmd_name -
+ *	 helper function to convert char representation to full command strings.
+ *
+ * cmd -  Valid values are '*', 'r', 'a', 'w' and 'd'.
+ *
+ */
+char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command");
+	}
+}
+
 /*
  * policy_role_list_to_array
  *	 helper function to convert a list of RoleSpecs to an array of
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 050eef97a4c..6aefbd0de07 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -33,12 +33,14 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
+#include "commands/policy.h"
 #include "common/keywords.h"
 #include "executor/spi.h"
 #include "funcapi.h"
@@ -13738,3 +13740,179 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * pretty - If pretty is true, the output includes tabs (\t) and newlines (\n).
+ * noOfTabChars - indent with specified no of tabs.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, bool pretty, int noOfTabChars, const char *fmt,...)
+{
+	va_list		args;
+
+	if (pretty)
+	{
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoString(buf, "\t");
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+
+	/* If pretty mode, append newline at the end */
+	if (pretty)
+		appendStringInfoChar(buf, '\n');
+}
+
+/*
+ * pg_get_policy_ddl
+ *
+ * Generate a CREATE POLICY statement for the specified policy.
+ *
+ * tableID - Table ID of the policy.
+ * policyName - Name of the policy for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	bool		pretty = PG_GETARG_BOOL(2);
+	HeapTuple	tuplePolicy;
+	Relation	pgPolicyRel;
+	Relation	targetTable;
+	Form_pg_policy policyForm;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	bool		attrIsNull;
+
+	StringInfoData buf;
+
+	initStringInfo(&buf);
+
+	targetTable = relation_open(tableID, NoLock);
+	/* Find policy to begin scan */
+	pgPolicyRel = table_open(PolicyRelationId, RowExclusiveLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(NameStr(*policyName)));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	/* Check that the policy is found, raise an error if not. */
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						NameStr(*policyName),
+						RelationGetRelationName(targetTable))));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	/* Build the CREATE POLICY statement */
+	get_formatted_string(&buf, pretty, 0, "CREATE POLICY %s ON %s",
+						 quote_identifier(NameStr(*policyName)),
+						 RelationGetRelationName(targetTable));
+
+	/* Check the type is PERMISSIVE or RESTRICTIVE */
+	get_formatted_string(&buf, pretty, 1,
+						 policyForm->polpermissive ? "AS PERMISSIVE" : "AS RESTRICTIVE");
+
+	/* Check command to which the policy applies */
+	get_formatted_string(&buf, pretty, 1, "FOR %s",
+						 get_policy_cmd_name(policyForm->polcmd));
+
+	/* Check if the policy has a TO list */
+	Datum		valueDatum = heap_getattr(tuplePolicy,
+										  Anum_pg_policy_polroles,
+										  RelationGetDescr(pgPolicyRel),
+										  &attrIsNull);
+
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (i > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, rolename);
+			}
+		}
+
+		if (role_names.len > 0)
+			get_formatted_string(&buf, pretty, 1, "TO %s", role_names.data);
+	}
+
+	/* Check if the policy has a USING expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *usingExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid, false);
+
+		get_formatted_string(&buf, pretty, 1, "USING (%s)",
+							 text_to_cstring(usingExpression));
+	}
+
+	/* Check if the policy has a WITH CHECK expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *checkExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid, false);
+
+		get_formatted_string(&buf, pretty, 1, "WITH CHECK (%s)",
+							 text_to_cstring(checkExpression));
+	}
+
+	appendStringInfoChar(&buf, ';');
+
+	/* Clean up. */
+	systable_endscan(sscan);
+	relation_close(targetTable, NoLock);
+	table_close(pgPolicyRel, RowExclusiveLock);
+
+	PG_RETURN_TEXT_P(string_to_text(buf.data));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b51d2b17379..d28039e4c08 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,9 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '8811', descr => 'get CREATE statement for policy',
+  proname => 'pg_get_policy_ddl', prorettype => 'text',
+  proargtypes => 'regclass name bool', prosrc => 'pg_get_policy_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h
index f06aa1df439..40e45b738f4 100644
--- a/src/include/commands/policy.h
+++ b/src/include/commands/policy.h
@@ -35,4 +35,6 @@ extern ObjectAddress rename_policy(RenameStmt *stmt);
 
 extern bool relation_has_policies(Relation rel);
 
+extern char *get_policy_cmd_name(char cmd);
+
 #endif							/* POLICY_H */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 5a172c5d91c..7763f31388d 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -4821,11 +4821,206 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1', false);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+SELECT pg_get_policy_ddl('tab1', NULL, false);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', NULL, false);
+                                 ^
+SELECT pg_get_policy_ddl(NULL, NULL, false);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+                                 ^
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1', false);
+ERROR:  policy "pol1" for table "rls_tbl_1" does not exist
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', false);
+                                                                       pg_get_policy_ddl                                                                       
+---------------------------------------------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE FOR ALL USING ((dlevel <= (SELECT rls_tbl_2.seclv FROM rls_tbl_2 WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', false);
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE FOR ALL USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', false);
+                                     pg_get_policy_ddl                                      
+--------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p3 ON rls_tbl_1 AS PERMISSIVE FOR ALL USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', false);
+                                  pg_get_policy_ddl                                   
+--------------------------------------------------------------------------------------
+  CREATE POLICY rls_p4 ON rls_tbl_1 AS PERMISSIVE FOR SELECT USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', false);
+                                     pg_get_policy_ddl                                     
+-------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p5 ON rls_tbl_1 AS PERMISSIVE FOR INSERT WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+                                  pg_get_policy_ddl                                   
+--------------------------------------------------------------------------------------
+  CREATE POLICY rls_p6 ON rls_tbl_1 AS PERMISSIVE FOR UPDATE USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+                               pg_get_policy_ddl                                
+--------------------------------------------------------------------------------
+  CREATE POLICY rls_p7 ON rls_tbl_1 AS PERMISSIVE FOR DELETE USING ((cid < 8));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+                                               pg_get_policy_ddl                                               
+---------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p8 ON rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+                                                                 pg_get_policy_ddl                                                                 
+---------------------------------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p9 ON rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_exempt_user WITH CHECK ((cid = (SELECT rls_tbl_2.seclv FROM rls_tbl_2)));
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	USING ((dlevel <= (SELECT rls_tbl_2.seclv FROM rls_tbl_2 WHERE (rls_tbl_2.pguser = CURRENT_USER))))
+;
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON rls_tbl_1
+	AS RESTRICTIVE
+	FOR ALL
+	USING (((cid <> 44) AND (cid < 50)))
+;
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	USING ((dauthor = CURRENT_USER))
+;
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR SELECT
+	USING (((cid % 2) = 0))
+;
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR INSERT
+	WITH CHECK (((cid % 2) = 1))
+;
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR UPDATE
+	USING (((cid % 2) = 0))
+;
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR DELETE
+	USING ((cid < 8))
+;
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_dave, regress_rls_alice
+	USING (true)
+;
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_exempt_user
+	WITH CHECK ((cid = (SELECT rls_tbl_2.seclv FROM rls_tbl_2)))
+;
+(1 row)
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 30 other objects
+NOTICE:  drop cascades to 32 other objects
 DETAIL:  drop cascades to function f_leak(text)
 drop cascades to table uaccount
 drop cascades to table category
@@ -4856,6 +5051,8 @@ drop cascades to table dep1
 drop cascades to table dep2
 drop cascades to table dob_t1
 drop cascades to table dob_t2
+drop cascades to table rls_tbl_1
+drop cascades to table rls_tbl_2
 DROP USER regress_rls_alice;
 DROP USER regress_rls_bob;
 DROP USER regress_rls_carol;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 21ac0ca51ee..75c99b01e27 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2400,6 +2400,84 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1', false);
+SELECT pg_get_policy_ddl('tab1', NULL, false);
+SELECT pg_get_policy_ddl(NULL, NULL, false);
+
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1', false);
+
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-15 17:25  Álvaro Herrera <[email protected]>
  parent: Akshay Joshi <[email protected]>
  1 sibling, 1 reply; 44+ messages in thread

From: Álvaro Herrera @ 2025-10-15 17:25 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: pgsql-hackers

Hello,

I have reviewed this patch before and provided a number of comments that
have been addressed by Akshay (so I encourage you to list my name and
this address in a Reviewed-by trailer line in the commit message).  One
thing I had not noticed is that while this function has a "pretty" flag,
it doesn't use it to pass anything to pg_get_expr_worker()'s prettyFlags
argument, and I think it should -- probably just 

  prettyFlags = GET_PRETTY_FLAGS(pretty);

same as pg_get_querydef() does.

Thanks

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/





^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-15 17:30  Philip Alger <[email protected]>
  parent: Akshay Joshi <[email protected]>
  1 sibling, 1 reply; 44+ messages in thread

From: Philip Alger @ 2025-10-15 17:30 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: pgsql-hackers

Hi Akshay,

When applying the patch, I got a number of errors and the tests failed. I
think it stems from:

+ targetTable = relation_open(tableID, NoLock);
+ relation_close(targetTable, NoLock);


I changed them to use "AccessShareLock" and it worked, and it's probably
relevant to change table_close to "AccessShareLock" as well. You're just
reading from the table.

+ table_close(pgPolicyRel, RowExclusiveLock);


You might move "Datum valueDatum" to the top of the function. The
compiler screamed at me for that, so I went ahead and made that change and
the screaming stopped.

+ /* Check if the policy has a TO list */
+ Datum valueDatum = heap_getattr(tuplePolicy,


I also don't think you need the extra parenthesis around "USING (%s)" and
""WITH CHECK (%s)" in the code; it seems to print it just fine without
them. Other people might have different opinions.

2) SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);   -- *pretty
> formatted DDL*
>                pg_get_policy_ddl
> ------------------------------------------------
>  CREATE POLICY rls_p8 ON rls_tbl_1
>          AS PERMISSIVE
>          FOR ALL
>          TO regress_rls_alice, regress_rls_dave
>          USING (true)
>  ;
>
>
As for the "pretty" part. In my opinion, I don't think it's necessary, and
putting the statement terminator (;) seems strange.
However, I think you're going to get a lot of opinions on what
well-formatted SQL looks like.

-- 
Best,
Phil Alger


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-16 08:17  Akshay Joshi <[email protected]>
  parent: Álvaro Herrera <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2025-10-16 08:17 UTC (permalink / raw)
  To: Álvaro Herrera <[email protected]>; +Cc: pgsql-hackers

On Wed, Oct 15, 2025 at 10:55 PM Álvaro Herrera <[email protected]>
wrote:

> Hello,
>
> I have reviewed this patch before and provided a number of comments that
> have been addressed by Akshay (so I encourage you to list my name and
> this address in a Reviewed-by trailer line in the commit message).  One
> thing I had not noticed is that while this function has a "pretty" flag,
> it doesn't use it to pass anything to pg_get_expr_worker()'s prettyFlags
> argument, and I think it should -- probably just
>
>   prettyFlags = GET_PRETTY_FLAGS(pretty);
>
> same as pg_get_querydef() does.
>

   Fixed and added 'Reviewed-by:'

>
> Thanks
>
> --
> Álvaro Herrera         PostgreSQL Developer  —
> https://www.EnterpriseDB.com/
>


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-16 08:34  Akshay Joshi <[email protected]>
  parent: Philip Alger <[email protected]>
  0 siblings, 2 replies; 44+ messages in thread

From: Akshay Joshi @ 2025-10-16 08:34 UTC (permalink / raw)
  To: Philip Alger <[email protected]>; +Cc: pgsql-hackers

On Wed, Oct 15, 2025 at 11:00 PM Philip Alger <[email protected]> wrote:

> Hi Akshay,
>
> When applying the patch, I got a number of errors and the tests failed. I
> think it stems from:
>
> + targetTable = relation_open(tableID, NoLock);
> + relation_close(targetTable, NoLock);
>
>
> I changed them to use "AccessShareLock" and it worked, and it's probably
> relevant to change table_close to "AccessShareLock" as well. You're just
> reading from the table.
>
> + table_close(pgPolicyRel, RowExclusiveLock);
>
>
> You might move "Datum valueDatum" to the top of the function. The
> compiler screamed at me for that, so I went ahead and made that change and
> the screaming stopped.
>
> + /* Check if the policy has a TO list */
> + Datum valueDatum = heap_getattr(tuplePolicy,
>
>
Fixed all the above review comments in the v2 patch.

>
> I also don't think you need the extra parenthesis around "USING (%s)" and
> ""WITH CHECK (%s)" in the code; it seems to print it just fine without
> them. Other people might have different opinions.
>

We need to add extra parentheses for the USING and CHECK clauses. Without
them, expressions like USING true or CHECK true will throw a syntax error
at or near "true".

>
> 2) SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);   -- *pretty
>> formatted DDL*
>>                pg_get_policy_ddl
>> ------------------------------------------------
>>  CREATE POLICY rls_p8 ON rls_tbl_1
>>          AS PERMISSIVE
>>          FOR ALL
>>          TO regress_rls_alice, regress_rls_dave
>>          USING (true)
>>  ;
>>
>>
> As for the "pretty" part. In my opinion, I don't think it's necessary, and
> putting the statement terminator (;) seems strange.
>

I think the pretty format option is a nice-to-have parameter. Users can
simply set it to false if they don’t want the DDL to be formatted.
As for the statement terminator, it’s useful to include it, while running
multiple queries together could result in a syntax error. In my opinion,
there’s no harm in providing the statement terminator.
However, I’ve modified the logic to add the statement terminator at the end
instead of appending to a new line.

>
> However, I think you're going to get a lot of opinions on what
> well-formatted SQL looks like.
>
> --
> Best,
> Phil Alger
>


Attachments:

  [application/octet-stream] v2-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch (22.7K, ../../CANxoLDdef6wW=T5czPSKPsk3xWeEHTeKxxxYMucmr-HURyoOgQ@mail.gmail.com/3-v2-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From cfc9ce4918ca05e4558104177f7d6320364a4642 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 10 Oct 2025 15:46:13 +0530
Subject: [PATCH v2] Add pg_get_policy_ddl() function to reconstruct CREATE
 POLICY statements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduces a new system function:
pg_get_policy_ddl(regclass table, name policy_name, bool pretty),
which reconstructs the CREATE POLICY statement for the specified policy.

Usage examples:
SELECT pg_get_policy_ddl('rls_table', 'pol1', false); -- non-pretty formatted DDL
SELECT pg_get_policy_ddl('rls_table', 'pol1', true);  -- pretty formatted DDL

Reference: PG-163
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  45 +++++
 src/backend/commands/policy.c             |  27 +++
 src/backend/utils/adt/ruleutils.c         | 186 ++++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   3 +
 src/include/commands/policy.h             |   2 +
 src/test/regress/expected/rowsecurity.out | 196 +++++++++++++++++++++-
 src/test/regress/sql/rowsecurity.sql      |  78 +++++++++
 7 files changed, 536 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c..4b9c661c20b 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </para>
+
+   <table id="functions-get-object-ddl-table">
+    <title>Get Object DDL Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>, <parameter>policy_name</parameter> <type>name</type>, <parameter>pretty</parameter> <type>boolean</type> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the CREATE POLICY statement from the system catalogs for a specified table and policy name.
+        When the pretty flag is set to true, the function returns a well-formatted DDL statement.
+        The result is a comprehensive <command>CREATE POLICY</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 83056960fe4..1abe0c44353 100644
--- a/src/backend/commands/policy.c
+++ b/src/backend/commands/policy.c
@@ -128,6 +128,33 @@ parse_policy_command(const char *cmd_name)
 	return polcmd;
 }
 
+/*
+ * get_policy_cmd_name -
+ *	 helper function to convert char representation to full command strings.
+ *
+ * cmd -  Valid values are '*', 'r', 'a', 'w' and 'd'.
+ *
+ */
+char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command");
+	}
+}
+
 /*
  * policy_role_list_to_array
  *	 helper function to convert a list of RoleSpecs to an array of
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 050eef97a4c..61fe35590f6 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -33,12 +33,14 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
+#include "commands/policy.h"
 #include "common/keywords.h"
 #include "executor/spi.h"
 #include "funcapi.h"
@@ -13738,3 +13740,187 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * pretty - If pretty is true, the output includes tabs (\t) and newlines (\n).
+ * noOfTabChars - indent with specified no of tabs.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, bool pretty, int noOfTabChars, const char *fmt,...)
+{
+	va_list		args;
+
+	if (pretty)
+	{
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoString(buf, "\t");
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+
+	/* If pretty mode, append newline at the end */
+	if (pretty)
+		appendStringInfoChar(buf, '\n');
+}
+
+/*
+ * pg_get_policy_ddl
+ *
+ * Generate a CREATE POLICY statement for the specified policy.
+ *
+ * tableID - Table ID of the policy.
+ * policyName - Name of the policy for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	bool		pretty = PG_GETARG_BOOL(2);
+	bool		attrIsNull;
+	int			prettyFlags;
+	Datum		valueDatum;
+	HeapTuple	tuplePolicy;
+	Relation	pgPolicyRel;
+	Relation	targetTable;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	Form_pg_policy policyForm;
+
+	StringInfoData buf;
+
+	initStringInfo(&buf);
+
+	targetTable = relation_open(tableID, AccessShareLock);
+	/* Find policy to begin scan */
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(NameStr(*policyName)));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	/* Check that the policy is found, raise an error if not. */
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						NameStr(*policyName),
+						RelationGetRelationName(targetTable))));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	/* Build the CREATE POLICY statement */
+	get_formatted_string(&buf, pretty, 0, "CREATE POLICY %s ON %s",
+						 quote_identifier(NameStr(*policyName)),
+						 RelationGetRelationName(targetTable));
+
+	/* Check the type is PERMISSIVE or RESTRICTIVE */
+	get_formatted_string(&buf, pretty, 1,
+						 policyForm->polpermissive ? "AS PERMISSIVE" : "AS RESTRICTIVE");
+
+	/* Check command to which the policy applies */
+	get_formatted_string(&buf, pretty, 1, "FOR %s",
+						 get_policy_cmd_name(policyForm->polcmd));
+
+	/* Check if the policy has a TO list */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (i > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, rolename);
+			}
+		}
+
+		if (role_names.len > 0)
+			get_formatted_string(&buf, pretty, 1, "TO %s", role_names.data);
+	}
+
+	prettyFlags = GET_PRETTY_FLAGS(pretty);
+	/* Check if the policy has a USING expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *usingExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, pretty, 1, "USING (%s)",
+							 text_to_cstring(usingExpression));
+	}
+
+	/* Check if the policy has a WITH CHECK expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *checkExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, pretty, 1, "WITH CHECK (%s)",
+							 text_to_cstring(checkExpression));
+	}
+
+	/* Replace '\n' with ';' if newline at the end */
+	if (buf.len > 0 && buf.data[buf.len - 1] == '\n')
+		buf.data[buf.len - 1] = ';';
+	else
+		appendStringInfoChar(&buf, ';');
+
+	/* Clean up. */
+	systable_endscan(sscan);
+	relation_close(targetTable, AccessShareLock);
+	table_close(pgPolicyRel, AccessShareLock);
+
+	PG_RETURN_TEXT_P(string_to_text(buf.data));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b51d2b17379..d28039e4c08 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,9 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '8811', descr => 'get CREATE statement for policy',
+  proname => 'pg_get_policy_ddl', prorettype => 'text',
+  proargtypes => 'regclass name bool', prosrc => 'pg_get_policy_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h
index f06aa1df439..40e45b738f4 100644
--- a/src/include/commands/policy.h
+++ b/src/include/commands/policy.h
@@ -35,4 +35,6 @@ extern ObjectAddress rename_policy(RenameStmt *stmt);
 
 extern bool relation_has_policies(Relation rel);
 
+extern char *get_policy_cmd_name(char cmd);
+
 #endif							/* POLICY_H */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 5a172c5d91c..992c88f2e3f 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -4821,11 +4821,203 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1', false);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+SELECT pg_get_policy_ddl('tab1', NULL, false);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', NULL, false);
+                                 ^
+SELECT pg_get_policy_ddl(NULL, NULL, false);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+                                 ^
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1', false);
+ERROR:  policy "pol1" for table "rls_tbl_1" does not exist
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', false);
+                                          pg_get_policy_ddl                                          
+-----------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE FOR ALL USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                                  +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', false);
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE FOR ALL USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', false);
+                                     pg_get_policy_ddl                                      
+--------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p3 ON rls_tbl_1 AS PERMISSIVE FOR ALL USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', false);
+                                  pg_get_policy_ddl                                   
+--------------------------------------------------------------------------------------
+  CREATE POLICY rls_p4 ON rls_tbl_1 AS PERMISSIVE FOR SELECT USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', false);
+                                     pg_get_policy_ddl                                     
+-------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p5 ON rls_tbl_1 AS PERMISSIVE FOR INSERT WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+                                  pg_get_policy_ddl                                   
+--------------------------------------------------------------------------------------
+  CREATE POLICY rls_p6 ON rls_tbl_1 AS PERMISSIVE FOR UPDATE USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+                               pg_get_policy_ddl                                
+--------------------------------------------------------------------------------
+  CREATE POLICY rls_p7 ON rls_tbl_1 AS PERMISSIVE FOR DELETE USING ((cid < 8));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+                                               pg_get_policy_ddl                                               
+---------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p8 ON rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+                                                        pg_get_policy_ddl                                                        
+---------------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p9 ON rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_exempt_user WITH CHECK ((cid = ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2)));
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON rls_tbl_1
+	AS RESTRICTIVE
+	FOR ALL
+	USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR SELECT
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR INSERT
+	WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR UPDATE
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR DELETE
+	USING (cid < 8);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_dave, regress_rls_alice
+	USING (true);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_exempt_user
+	WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 30 other objects
+NOTICE:  drop cascades to 32 other objects
 DETAIL:  drop cascades to function f_leak(text)
 drop cascades to table uaccount
 drop cascades to table category
@@ -4856,6 +5048,8 @@ drop cascades to table dep1
 drop cascades to table dep2
 drop cascades to table dob_t1
 drop cascades to table dob_t2
+drop cascades to table rls_tbl_1
+drop cascades to table rls_tbl_2
 DROP USER regress_rls_alice;
 DROP USER regress_rls_bob;
 DROP USER regress_rls_carol;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 21ac0ca51ee..75c99b01e27 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2400,6 +2400,84 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1', false);
+SELECT pg_get_policy_ddl('tab1', NULL, false);
+SELECT pg_get_policy_ddl(NULL, NULL, false);
+
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1', false);
+
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-16 09:14  jian he <[email protected]>
  parent: Akshay Joshi <[email protected]>
  1 sibling, 1 reply; 44+ messages in thread

From: jian he @ 2025-10-16 09:14 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Philip Alger <[email protected]>; pgsql-hackers

hi. I still can not compile your v2.

../../Desktop/pg_src/src1/postgres/src/backend/utils/adt/ruleutils.c:
In function ‘get_formatted_string’:
../../Desktop/pg_src/src1/postgres/src/backend/utils/adt/ruleutils.c:13770:9:
error: function ‘get_formatted_string’ might be a candidate for
‘gnu_printf’ format attribute [-Werror=suggest-attribute=format]
13770 |         appendStringInfoVA(buf, fmt, args);
      |         ^~~~~~~~~~~~~~~~~~
cc1: all warnings being treated as errors

Maybe you can register your patch on https://commitfest.postgresql.org/
then it will run all CI tests on all kinds of OS.

row security policy qual and with_check_qual can contain sublink/subquery.
but pg_get_expr can not cope with sublink/subquery.

see pg_get_expr comments below:
 * Currently, the expression can only refer to a single relation, namely
 * the one specified by the second parameter.  This is sufficient for
 * partial indexes, column default expressions, etc.  We also support
 * Var-free expressions, for which the OID can be InvalidOid.

see commit 6867f96 and
https://www.postgresql.org/message-id/flat/20211219205422.GT17618%40telsasoft.com

I guess (because I can not compile, mentioned above):
"ERROR:  expression contains variables"
can be triggered by the following setup:

create table t(a int);
CREATE POLICY p1 ON t AS RESTRICTIVE FOR ALL
USING (a IS NOT NULL AND (SELECT 1 = 1 FROM pg_rewrite WHERE
pg_get_function_arg_default(ev_class, 1) !~~ pg_get_expr(ev_qual, 0,
false)));
SELECT pg_get_policy_ddl('t', 'p1', true);

You can also check my patch at https://commitfest.postgresql.org/patch/6054/
which similarly needs to build the POLICY definition for reconstruction.

see RelationBuildRowSecurity, checkExprHasSubLink also.





^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-16 11:47  Akshay Joshi <[email protected]>
  parent: jian he <[email protected]>
  0 siblings, 0 replies; 44+ messages in thread

From: Akshay Joshi @ 2025-10-16 11:47 UTC (permalink / raw)
  To: jian he <[email protected]>; +Cc: Philip Alger <[email protected]>; pgsql-hackers

On Thu, Oct 16, 2025 at 2:45 PM jian he <[email protected]> wrote:

> hi. I still can not compile your v2.
>
> ../../Desktop/pg_src/src1/postgres/src/backend/utils/adt/ruleutils.c:
> In function ‘get_formatted_string’:
>
> ../../Desktop/pg_src/src1/postgres/src/backend/utils/adt/ruleutils.c:13770:9:
> error: function ‘get_formatted_string’ might be a candidate for
> ‘gnu_printf’ format attribute [-Werror=suggest-attribute=format]
> 13770 |         appendStringInfoVA(buf, fmt, args);
>       |         ^~~~~~~~~~~~~~~~~~
> cc1: all warnings being treated as errors
>

I’m relatively new to PostgreSQL development. I’m working on setting up the
CI pipeline and will try to fix all warnings.

>
> Maybe you can register your patch on https://commitfest.postgresql.org/
> then it will run all CI tests on all kinds of OS.
>
> row security policy qual and with_check_qual can contain sublink/subquery.
> but pg_get_expr can not cope with sublink/subquery.
>
> see pg_get_expr comments below:
>  * Currently, the expression can only refer to a single relation, namely
>  * the one specified by the second parameter.  This is sufficient for
>  * partial indexes, column default expressions, etc.  We also support
>  * Var-free expressions, for which the OID can be InvalidOid.
>
> see commit 6867f96 and
>
> https://www.postgresql.org/message-id/flat/20211219205422.GT17618%40telsasoft.com
>
> I guess (because I can not compile, mentioned above):
> "ERROR:  expression contains variables"
> can be triggered by the following setup:
>
> create table t(a int);
> CREATE POLICY p1 ON t AS RESTRICTIVE FOR ALL
> USING (a IS NOT NULL AND (SELECT 1 = 1 FROM pg_rewrite WHERE
> pg_get_function_arg_default(ev_class, 1) !~~ pg_get_expr(ev_qual, 0,
> false)));
> SELECT pg_get_policy_ddl('t', 'p1', true);
>

The above example works fine with my patch
[image: Screenshot 2025-10-16 at 5.08.10 PM.png]


>
> You can also check my patch at
> https://commitfest.postgresql.org/patch/6054/
> which similarly needs to build the POLICY definition for reconstruction.
>
> see RelationBuildRowSecurity, checkExprHasSubLink also.
>


Attachments:

  [image/png] Screenshot 2025-10-16 at 5.08.10 PM.png (370.6K, ../../CANxoLDeAqK9d5iC4Ou9GuyRfNGvXyAm1=cYysGKTTTpaUj1Gyw@mail.gmail.com/3-Screenshot%202025-10-16%20at%205.08.10%E2%80%AFPM.png)
  download | view image

^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-16 12:36  Philip Alger <[email protected]>
  parent: Akshay Joshi <[email protected]>
  1 sibling, 1 reply; 44+ messages in thread

From: Philip Alger @ 2025-10-16 12:36 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: pgsql-hackers

Hi Akshay,


>>> As for the statement terminator, it’s useful to include it, while
>> running multiple queries together could result in a syntax error. In my
>> opinion, there’s no harm in providing the statement terminator.
>>
> However, I’ve modified the logic to add the statement terminator at the
> end instead of appending to a new line.
>
>>
>>
Regarding putting the terminator at the end, I think my original comment
got cut off by my poor editing. Yes, that's what I was referring to; no
need to append it to a new line.

-- 
Best, Phil Alger


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-16 12:50  Akshay Joshi <[email protected]>
  parent: Philip Alger <[email protected]>
  0 siblings, 2 replies; 44+ messages in thread

From: Akshay Joshi @ 2025-10-16 12:50 UTC (permalink / raw)
  To: Philip Alger <[email protected]>; +Cc: pgsql-hackers

Please find attached the v3 patch, which resolves all compilation errors
and warnings.

On Thu, Oct 16, 2025 at 6:06 PM Philip Alger <[email protected]> wrote:

> Hi Akshay,
>
>
>>>> As for the statement terminator, it’s useful to include it, while
>>> running multiple queries together could result in a syntax error. In my
>>> opinion, there’s no harm in providing the statement terminator.
>>>
>> However, I’ve modified the logic to add the statement terminator at the
>> end instead of appending to a new line.
>>
>>>
>>>
> Regarding putting the terminator at the end, I think my original comment
> got cut off by my poor editing. Yes, that's what I was referring to; no
> need to append it to a new line.
>
> --
> Best, Phil Alger
>


Attachments:

  [application/octet-stream] v3-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch (23.1K, ../../CANxoLDcGLSHDj8Ve0qyM2UWMdgSFJA-28j7dEAtMBey8D_ktdA@mail.gmail.com/3-v3-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From dab0fee58e2887e0d235238e8d3b0609105ebb0a Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 10 Oct 2025 15:46:13 +0530
Subject: [PATCH v3] Add pg_get_policy_ddl() function to reconstruct CREATE
 POLICY statements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduces a new system function:
pg_get_policy_ddl(regclass table, name policy_name, bool pretty),
which reconstructs the CREATE POLICY statement for the specified policy.

Usage examples:
SELECT pg_get_policy_ddl('rls_table', 'pol1', false); -- non-pretty formatted DDL
SELECT pg_get_policy_ddl('rls_table', 'pol1', true);  -- pretty formatted DDL

Reference: PG-163
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
Reviewed-by: Philip Alger <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  45 +++++
 src/backend/commands/policy.c             |  27 +++
 src/backend/utils/adt/ruleutils.c         | 190 +++++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   3 +
 src/include/commands/policy.h             |   2 +
 src/test/regress/expected/rowsecurity.out | 196 +++++++++++++++++++++-
 src/test/regress/sql/rowsecurity.sql      |  78 +++++++++
 7 files changed, 540 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c..4b9c661c20b 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </para>
+
+   <table id="functions-get-object-ddl-table">
+    <title>Get Object DDL Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>, <parameter>policy_name</parameter> <type>name</type>, <parameter>pretty</parameter> <type>boolean</type> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the CREATE POLICY statement from the system catalogs for a specified table and policy name.
+        When the pretty flag is set to true, the function returns a well-formatted DDL statement.
+        The result is a comprehensive <command>CREATE POLICY</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 83056960fe4..1abe0c44353 100644
--- a/src/backend/commands/policy.c
+++ b/src/backend/commands/policy.c
@@ -128,6 +128,33 @@ parse_policy_command(const char *cmd_name)
 	return polcmd;
 }
 
+/*
+ * get_policy_cmd_name -
+ *	 helper function to convert char representation to full command strings.
+ *
+ * cmd -  Valid values are '*', 'r', 'a', 'w' and 'd'.
+ *
+ */
+char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command");
+	}
+}
+
 /*
  * policy_role_list_to_array
  *	 helper function to convert a list of RoleSpecs to an array of
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 050eef97a4c..4d8611042c5 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -33,12 +33,14 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
+#include "commands/policy.h"
 #include "common/keywords.h"
 #include "executor/spi.h"
 #include "funcapi.h"
@@ -546,6 +548,10 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
 										  deparse_context *context,
 										  bool showimplicit,
 										  bool needcomma);
+static void get_formatted_string(StringInfo buf,
+								 bool pretty,
+								 int noOfTabChars,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13738,3 +13744,187 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * pretty - If pretty is true, the output includes tabs (\t) and newlines (\n).
+ * noOfTabChars - indent with specified no of tabs.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, bool pretty, int noOfTabChars, const char *fmt,...)
+{
+	va_list		args;
+
+	if (pretty)
+	{
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoString(buf, "\t");
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+
+	/* If pretty mode, append newline at the end */
+	if (pretty)
+		appendStringInfoChar(buf, '\n');
+}
+
+/*
+ * pg_get_policy_ddl
+ *
+ * Generate a CREATE POLICY statement for the specified policy.
+ *
+ * tableID - Table ID of the policy.
+ * policyName - Name of the policy for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	bool		pretty = PG_GETARG_BOOL(2);
+	bool		attrIsNull;
+	int			prettyFlags;
+	Datum		valueDatum;
+	HeapTuple	tuplePolicy;
+	Relation	pgPolicyRel;
+	Relation	targetTable;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	Form_pg_policy policyForm;
+
+	StringInfoData buf;
+
+	initStringInfo(&buf);
+
+	targetTable = relation_open(tableID, AccessShareLock);
+	/* Find policy to begin scan */
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(NameStr(*policyName)));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	/* Check that the policy is found, raise an error if not. */
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						NameStr(*policyName),
+						RelationGetRelationName(targetTable))));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	/* Build the CREATE POLICY statement */
+	get_formatted_string(&buf, pretty, 0, "CREATE POLICY %s ON %s",
+						 quote_identifier(NameStr(*policyName)),
+						 RelationGetRelationName(targetTable));
+
+	/* Check the type is PERMISSIVE or RESTRICTIVE */
+	get_formatted_string(&buf, pretty, 1,
+						 policyForm->polpermissive ? "AS PERMISSIVE" : "AS RESTRICTIVE");
+
+	/* Check command to which the policy applies */
+	get_formatted_string(&buf, pretty, 1, "FOR %s",
+						 get_policy_cmd_name(policyForm->polcmd));
+
+	/* Check if the policy has a TO list */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (i > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, rolename);
+			}
+		}
+
+		if (role_names.len > 0)
+			get_formatted_string(&buf, pretty, 1, "TO %s", role_names.data);
+	}
+
+	prettyFlags = GET_PRETTY_FLAGS(pretty);
+	/* Check if the policy has a USING expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *usingExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, pretty, 1, "USING (%s)",
+							 text_to_cstring(usingExpression));
+	}
+
+	/* Check if the policy has a WITH CHECK expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *checkExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, pretty, 1, "WITH CHECK (%s)",
+							 text_to_cstring(checkExpression));
+	}
+
+	/* Replace '\n' with ';' if newline at the end */
+	if (buf.len > 0 && buf.data[buf.len - 1] == '\n')
+		buf.data[buf.len - 1] = ';';
+	else
+		appendStringInfoChar(&buf, ';');
+
+	/* Clean up. */
+	systable_endscan(sscan);
+	relation_close(targetTable, AccessShareLock);
+	table_close(pgPolicyRel, AccessShareLock);
+
+	PG_RETURN_TEXT_P(string_to_text(buf.data));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b51d2b17379..d28039e4c08 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,9 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '8811', descr => 'get CREATE statement for policy',
+  proname => 'pg_get_policy_ddl', prorettype => 'text',
+  proargtypes => 'regclass name bool', prosrc => 'pg_get_policy_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h
index f06aa1df439..40e45b738f4 100644
--- a/src/include/commands/policy.h
+++ b/src/include/commands/policy.h
@@ -35,4 +35,6 @@ extern ObjectAddress rename_policy(RenameStmt *stmt);
 
 extern bool relation_has_policies(Relation rel);
 
+extern char *get_policy_cmd_name(char cmd);
+
 #endif							/* POLICY_H */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 5a172c5d91c..992c88f2e3f 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -4821,11 +4821,203 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1', false);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+SELECT pg_get_policy_ddl('tab1', NULL, false);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', NULL, false);
+                                 ^
+SELECT pg_get_policy_ddl(NULL, NULL, false);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+                                 ^
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1', false);
+ERROR:  policy "pol1" for table "rls_tbl_1" does not exist
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', false);
+                                          pg_get_policy_ddl                                          
+-----------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE FOR ALL USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                                  +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', false);
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE FOR ALL USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', false);
+                                     pg_get_policy_ddl                                      
+--------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p3 ON rls_tbl_1 AS PERMISSIVE FOR ALL USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', false);
+                                  pg_get_policy_ddl                                   
+--------------------------------------------------------------------------------------
+  CREATE POLICY rls_p4 ON rls_tbl_1 AS PERMISSIVE FOR SELECT USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', false);
+                                     pg_get_policy_ddl                                     
+-------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p5 ON rls_tbl_1 AS PERMISSIVE FOR INSERT WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+                                  pg_get_policy_ddl                                   
+--------------------------------------------------------------------------------------
+  CREATE POLICY rls_p6 ON rls_tbl_1 AS PERMISSIVE FOR UPDATE USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+                               pg_get_policy_ddl                                
+--------------------------------------------------------------------------------
+  CREATE POLICY rls_p7 ON rls_tbl_1 AS PERMISSIVE FOR DELETE USING ((cid < 8));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+                                               pg_get_policy_ddl                                               
+---------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p8 ON rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+                                                        pg_get_policy_ddl                                                        
+---------------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p9 ON rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_exempt_user WITH CHECK ((cid = ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2)));
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON rls_tbl_1
+	AS RESTRICTIVE
+	FOR ALL
+	USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR SELECT
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR INSERT
+	WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR UPDATE
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR DELETE
+	USING (cid < 8);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_dave, regress_rls_alice
+	USING (true);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_exempt_user
+	WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 30 other objects
+NOTICE:  drop cascades to 32 other objects
 DETAIL:  drop cascades to function f_leak(text)
 drop cascades to table uaccount
 drop cascades to table category
@@ -4856,6 +5048,8 @@ drop cascades to table dep1
 drop cascades to table dep2
 drop cascades to table dob_t1
 drop cascades to table dob_t2
+drop cascades to table rls_tbl_1
+drop cascades to table rls_tbl_2
 DROP USER regress_rls_alice;
 DROP USER regress_rls_bob;
 DROP USER regress_rls_carol;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 21ac0ca51ee..75c99b01e27 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2400,6 +2400,84 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1', false);
+SELECT pg_get_policy_ddl('tab1', NULL, false);
+SELECT pg_get_policy_ddl(NULL, NULL, false);
+
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1', false);
+
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-21 09:08  Chao Li <[email protected]>
  parent: Akshay Joshi <[email protected]>
  1 sibling, 1 reply; 44+ messages in thread

From: Chao Li @ 2025-10-21 09:08 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Philip Alger <[email protected]>; pgsql-hackers



> On Oct 16, 2025, at 20:50, Akshay Joshi <[email protected]> wrote:
> 
> Please find attached the v3 patch, which resolves all compilation errors and warnings.
> 
> On Thu, Oct 16, 2025 at 6:06 PM Philip Alger <[email protected]> wrote:
> Hi Akshay,
> 
> 
> As for the statement terminator, it’s useful to include it, while running multiple queries together could result in a syntax error. In my opinion, there’s no harm in providing the statement terminator.
> However, I’ve modified the logic to add the statement terminator at the end instead of appending to a new line. 
> 
> 
> Regarding putting the terminator at the end, I think my original comment got cut off by my poor editing. Yes, that's what I was referring to; no need to append it to a new line. 
> 
> -- 
> Best, Phil Alger
> <v3-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch>

1 - ruleutils.c
```
+	if (pretty)
+	{
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoString(buf, "\t");
+		}
```

As you are adding a single char of ‘\t’, better to use appendStringInfoChar() that is cheaper.

2 - ruleutils.c
```
+	initStringInfo(&buf);
+
+	targetTable = relation_open(tableID, AccessShareLock);
```

Looks like only usage of opening the table is to get the table name. In that case, why don’t just call get_rel_name(tableID) and store the table name in a local variable?

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/









^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-22 07:20  jian he <[email protected]>
  parent: Akshay Joshi <[email protected]>
  1 sibling, 1 reply; 44+ messages in thread

From: jian he @ 2025-10-22 07:20 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Philip Alger <[email protected]>; pgsql-hackers

On Thu, Oct 16, 2025 at 8:51 PM Akshay Joshi
<[email protected]> wrote:
>
> Please find attached the v3 patch, which resolves all compilation errors and warnings.
>

drop table if exists t, ts, ts1;
create table t(a int);
CREATE POLICY p0 ON t FOR ALL TO PUBLIC USING (a % 2 = 1);
SELECT pg_get_policy_ddl('t', 'p0', false);

                          pg_get_policy_ddl
---------------------------------------------------------------------
  CREATE POLICY p0 ON t AS PERMISSIVE FOR ALL USING (((a % 2) = 1));
(1 row)

"TO PUBLIC" part is missing, maybe it's ok.


SELECT pg_get_policy_ddl(-1, 'p0', false);
ERROR:  could not open relation with OID 4294967295
as I mentioned in a nearby thread [1], this should be NULL instead of ERROR.
[1] https://postgr.es/m/CACJufxGbE4uJWu1YuqdmOx+7PMBpHvX_fbRMmHu=r4SrsuW9tg@mail.gmail.com


IMHO, get_formatted_string is not needed, most of the time, if pretty is true,
we append "\t" and "\n", for that we can simply do
```
  appendStringInfo(&buf, "CREATE POLICY %s ON %s ",
       quote_identifier(NameStr(*policyName)),
       generate_qualified_relation_name(policy_form->polrelid));
if (pretty)
    appendStringInfoString(buf, "\t\n");
```


in pg_get_triggerdef_worker, I found the below code pattern:
    /*
     * In non-pretty mode, always schema-qualify the target table name for
     * safety.  In pretty mode, schema-qualify only if not visible.
     */
    appendStringInfo(&buf, " ON %s ",
                     pretty ?
                     generate_relation_name(trigrec->tgrelid, NIL) :
                     generate_qualified_relation_name(trigrec->tgrelid));

maybe we can apply it too while construct query string:
"CREATE POLICY %s ON %s",





^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-22 13:19  Akshay Joshi <[email protected]>
  parent: Chao Li <[email protected]>
  0 siblings, 0 replies; 44+ messages in thread

From: Akshay Joshi @ 2025-10-22 13:19 UTC (permalink / raw)
  To: Chao Li <[email protected]>; +Cc: Philip Alger <[email protected]>; pgsql-hackers

On Tue, Oct 21, 2025 at 2:39 PM Chao Li <[email protected]> wrote:

>
>
> > On Oct 16, 2025, at 20:50, Akshay Joshi <[email protected]>
> wrote:
> >
> > Please find attached the v3 patch, which resolves all compilation errors
> and warnings.
> >
> > On Thu, Oct 16, 2025 at 6:06 PM Philip Alger <[email protected]> wrote:
> > Hi Akshay,
> >
> >
> > As for the statement terminator, it’s useful to include it, while
> running multiple queries together could result in a syntax error. In my
> opinion, there’s no harm in providing the statement terminator.
> > However, I’ve modified the logic to add the statement terminator at the
> end instead of appending to a new line.
> >
> >
> > Regarding putting the terminator at the end, I think my original comment
> got cut off by my poor editing. Yes, that's what I was referring to; no
> need to append it to a new line.
> >
> > --
> > Best, Phil Alger
> > <v3-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch>
>
> 1 - ruleutils.c
> ```
> +       if (pretty)
> +       {
> +               /* Indent with tabs */
> +               for (int i = 0; i < noOfTabChars; i++)
> +               {
> +                       appendStringInfoString(buf, "\t");
> +               }
> ```
>
> As you are adding a single char of ‘\t’, better to use
> appendStringInfoChar() that is cheaper.
>
> 2 - ruleutils.c
> ```
> +       initStringInfo(&buf);
> +
> +       targetTable = relation_open(tableID, AccessShareLock);
> ```
>
> Looks like only usage of opening the table is to get the table name. In
> that case, why don’t just call get_rel_name(tableID) and store the table
> name in a local variable?
>

    Fixed the above review comments in the v4 patch. I have used
generate_qualified_relation_name(tableID) instead of get_rel_name(tableID).

>
> Best regards,
> --
> Chao Li (Evan)
> HighGo Software Co., Ltd.
> https://www.highgo.com/
>
>
>
>
>


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-22 13:21  Akshay Joshi <[email protected]>
  parent: jian he <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2025-10-22 13:21 UTC (permalink / raw)
  To: jian he <[email protected]>; +Cc: Philip Alger <[email protected]>; pgsql-hackers

On Wed, Oct 22, 2025 at 12:51 PM jian he <[email protected]>
wrote:

> On Thu, Oct 16, 2025 at 8:51 PM Akshay Joshi
> <[email protected]> wrote:
> >
> > Please find attached the v3 patch, which resolves all compilation errors
> and warnings.
> >
>
> drop table if exists t, ts, ts1;
> create table t(a int);
> CREATE POLICY p0 ON t FOR ALL TO PUBLIC USING (a % 2 = 1);
> SELECT pg_get_policy_ddl('t', 'p0', false);
>
>                           pg_get_policy_ddl
> ---------------------------------------------------------------------
>   CREATE POLICY p0 ON t AS PERMISSIVE FOR ALL USING (((a % 2) = 1));
> (1 row)
>
> "TO PUBLIC" part is missing, maybe it's ok.
>

I used the logic below, which did not return PUBLIC as a role. I have added
logic to default the TO clause to PUBLIC when no specific role name is
provided
valueDatum = heap_getattr(tuplePolicy,
Anum_pg_policy_polroles,
RelationGetDescr(pgPolicyRel),
&attrIsNull);
if (!attrIsNull)
{
ArrayType *policy_roles = DatumGetArrayTypePCopy(valueDatum);
int nitems = ARR_DIMS(policy_roles)[0];
Oid *roles = (Oid *) ARR_DATA_PTR(policy_roles);


>
>
> SELECT pg_get_policy_ddl(-1, 'p0', false);
> ERROR:  could not open relation with OID 4294967295
> as I mentioned in a nearby thread [1], this should be NULL instead of
> ERROR.
> [1]
> https://postgr.es/m/CACJufxGbE4uJWu1YuqdmOx+7PMBpHvX_fbRMmHu=r4SrsuW9tg@mail.gmail.com
>
> Fixed in v4 patch.

>
> IMHO, get_formatted_string is not needed, most of the time, if pretty is
> true,
> we append "\t" and "\n", for that we can simply do
> ```
>   appendStringInfo(&buf, "CREATE POLICY %s ON %s ",
>        quote_identifier(NameStr(*policyName)),
>        generate_qualified_relation_name(policy_form->polrelid));
> if (pretty)
>     appendStringInfoString(buf, "\t\n");
> ```
>
>
The get_formatted_string function is needed. Instead of using multiple if
statements for the pretty flag, it’s better to have a generic function.
This will be useful if the pretty-format DDL implementation is accepted by
the community, as it can be reused by other pg_get_<object>_ddl() DDL
functions added in the future.

>
> in pg_get_triggerdef_worker, I found the below code pattern:
>     /*
>      * In non-pretty mode, always schema-qualify the target table name for
>      * safety.  In pretty mode, schema-qualify only if not visible.
>      */
>     appendStringInfo(&buf, " ON %s ",
>                      pretty ?
>                      generate_relation_name(trigrec->tgrelid, NIL) :
>                      generate_qualified_relation_name(trigrec->tgrelid));
>
> maybe we can apply it too while construct query string:
> "CREATE POLICY %s ON %s",
>

In my opinion, the table name should always be schema-qualified, which I
have addressed in the v4 patch. The implementation of the pretty flag here
differs from pg_get_triggerdef_worker, which is used only for the target
table name. In my patch, the pretty flag adds \t and \n to each different
clause (example AS, FOR, USING ...)


Attachments:

  [application/octet-stream] v4-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch (24.5K, ../../CANxoLDccMKZXA7qWAu6bGXRqVGu_DNPFxP4ssQ5Q4yq9Hwiq-g@mail.gmail.com/3-v4-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From a86ff9fc2fe8f0b903beb6aaec1cd8e8667bbc4a Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 10 Oct 2025 15:46:13 +0530
Subject: [PATCH v4] Add pg_get_policy_ddl() function to reconstruct CREATE
 POLICY statements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduces a new system function:
pg_get_policy_ddl(regclass table, name policy_name, bool pretty),
which reconstructs the CREATE POLICY statement for the specified policy.

Usage examples:
SELECT pg_get_policy_ddl('rls_table', 'pol1', false); -- non-pretty formatted DDL
SELECT pg_get_policy_ddl('rls_table', 'pol1', true);  -- pretty formatted DDL

Reference: PG-163
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
Reviewed-by: Philip Alger <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  45 +++++
 src/backend/commands/policy.c             |  27 +++
 src/backend/utils/adt/ruleutils.c         | 199 ++++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   3 +
 src/include/commands/policy.h             |   2 +
 src/test/regress/expected/rowsecurity.out | 210 +++++++++++++++++++++-
 src/test/regress/sql/rowsecurity.sql      |  80 +++++++++
 7 files changed, 565 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c..4b9c661c20b 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </para>
+
+   <table id="functions-get-object-ddl-table">
+    <title>Get Object DDL Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>, <parameter>policy_name</parameter> <type>name</type>, <parameter>pretty</parameter> <type>boolean</type> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the CREATE POLICY statement from the system catalogs for a specified table and policy name.
+        When the pretty flag is set to true, the function returns a well-formatted DDL statement.
+        The result is a comprehensive <command>CREATE POLICY</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 83056960fe4..1abe0c44353 100644
--- a/src/backend/commands/policy.c
+++ b/src/backend/commands/policy.c
@@ -128,6 +128,33 @@ parse_policy_command(const char *cmd_name)
 	return polcmd;
 }
 
+/*
+ * get_policy_cmd_name -
+ *	 helper function to convert char representation to full command strings.
+ *
+ * cmd -  Valid values are '*', 'r', 'a', 'w' and 'd'.
+ *
+ */
+char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command");
+	}
+}
+
 /*
  * policy_role_list_to_array
  *	 helper function to convert a list of RoleSpecs to an array of
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 79ec136231b..c05e4786703 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -33,12 +33,14 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
+#include "commands/policy.h"
 #include "common/keywords.h"
 #include "executor/spi.h"
 #include "funcapi.h"
@@ -546,6 +548,10 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
 										  deparse_context *context,
 										  bool showimplicit,
 										  bool needcomma);
+static void get_formatted_string(StringInfo buf,
+								 bool pretty,
+								 int noOfTabChars,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13738,3 +13744,196 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * pretty - If pretty is true, the output includes tabs (\t) and newlines (\n).
+ * noOfTabChars - indent with specified no of tabs.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, bool pretty, int noOfTabChars, const char *fmt,...)
+{
+	va_list		args;
+
+	if (pretty)
+	{
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+
+	/* If pretty mode, append newline at the end */
+	if (pretty)
+		appendStringInfoChar(buf, '\n');
+}
+
+/*
+ * pg_get_policy_ddl
+ *
+ * Generate a CREATE POLICY statement for the specified policy.
+ *
+ * tableID - Table ID of the policy.
+ * policyName - Name of the policy for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	bool		pretty = PG_GETARG_BOOL(2);
+	bool		attrIsNull;
+	int			prettyFlags;
+	Datum		valueDatum;
+	HeapTuple	tuplePolicy;
+	Relation	pgPolicyRel;
+	char	   *targetTable;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	Form_pg_policy policyForm;
+	StringInfoData buf;
+
+	/* Validate that the relation exists */
+	if (!OidIsValid(tableID) || get_rel_name(tableID) == NULL)
+		PG_RETURN_NULL();
+
+	initStringInfo(&buf);
+
+	targetTable = generate_qualified_relation_name(tableID);
+	/* Find policy to begin scan */
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(NameStr(*policyName)));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	/* Check that the policy is found, raise an error if not. */
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						NameStr(*policyName),
+						targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	/* Build the CREATE POLICY statement */
+	get_formatted_string(&buf, pretty, 0, "CREATE POLICY %s ON %s",
+						 quote_identifier(NameStr(*policyName)),
+						 targetTable);
+
+	/* Check the type is PERMISSIVE or RESTRICTIVE */
+	get_formatted_string(&buf, pretty, 1,
+						 policyForm->polpermissive ? "AS PERMISSIVE" : "AS RESTRICTIVE");
+
+	/* Check command to which the policy applies */
+	get_formatted_string(&buf, pretty, 1, "FOR %s",
+						 get_policy_cmd_name(policyForm->polcmd));
+
+	/* Check if the policy has a TO list */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (i > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, rolename);
+			}
+		}
+
+		if (role_names.len > 0)
+			get_formatted_string(&buf, pretty, 1, "TO %s", role_names.data);
+		else
+
+			/*
+			 * When no specific role is provided, generate the TO clause with
+			 * the PUBLIC role.
+			 */
+			get_formatted_string(&buf, pretty, 1, "TO PUBLIC");
+	}
+
+	prettyFlags = GET_PRETTY_FLAGS(pretty);
+	/* Check if the policy has a USING expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *usingExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, pretty, 1, "USING (%s)",
+							 text_to_cstring(usingExpression));
+	}
+
+	/* Check if the policy has a WITH CHECK expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *checkExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, pretty, 1, "WITH CHECK (%s)",
+							 text_to_cstring(checkExpression));
+	}
+
+	/* Replace '\n' with ';' if newline at the end */
+	if (buf.len > 0 && buf.data[buf.len - 1] == '\n')
+		buf.data[buf.len - 1] = ';';
+	else
+		appendStringInfoChar(&buf, ';');
+
+	/* Clean up. */
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+
+	PG_RETURN_TEXT_P(string_to_text(buf.data));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index eecb43ec6f0..536c5a857da 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,9 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '8811', descr => 'get CREATE statement for policy',
+  proname => 'pg_get_policy_ddl', prorettype => 'text',
+  proargtypes => 'regclass name bool', prosrc => 'pg_get_policy_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h
index f06aa1df439..40e45b738f4 100644
--- a/src/include/commands/policy.h
+++ b/src/include/commands/policy.h
@@ -35,4 +35,6 @@ extern ObjectAddress rename_policy(RenameStmt *stmt);
 
 extern bool relation_has_policies(Relation rel);
 
+extern char *get_policy_cmd_name(char cmd);
+
 #endif							/* POLICY_H */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 42b78a24603..815a4ff72ce 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -4842,11 +4842,217 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1', false);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+SELECT pg_get_policy_ddl('tab1', NULL, false);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', NULL, false);
+                                 ^
+SELECT pg_get_policy_ddl(NULL, NULL, false);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Test -1 as table oid
+ SELECT pg_get_policy_ddl(-1, 'rls_p1', false);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+                                 ^
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1', false);
+ERROR:  policy "pol1" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', false);
+                                                        pg_get_policy_ddl                                                         
+----------------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO PUBLIC USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                                                               +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', false);
+                                                      pg_get_policy_ddl                                                       
+------------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE FOR ALL TO PUBLIC USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', false);
+                                                    pg_get_policy_ddl                                                    
+-------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO PUBLIC USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', false);
+                                                 pg_get_policy_ddl                                                 
+-------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR SELECT TO PUBLIC USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', false);
+                                                   pg_get_policy_ddl                                                    
+------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR INSERT TO PUBLIC WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+                                                 pg_get_policy_ddl                                                 
+-------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR UPDATE TO PUBLIC USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+                                              pg_get_policy_ddl                                              
+-------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR DELETE TO PUBLIC USING ((cid < 8));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+                                                        pg_get_policy_ddl                                                         
+----------------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+                                                                 pg_get_policy_ddl                                                                  
+----------------------------------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_exempt_user WITH CHECK ((cid = ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2)));
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO PUBLIC
+	USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+	AS RESTRICTIVE
+	FOR ALL
+	TO PUBLIC
+	USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO PUBLIC
+	USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR SELECT
+	TO PUBLIC
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR INSERT
+	TO PUBLIC
+	WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR UPDATE
+	TO PUBLIC
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR DELETE
+	TO PUBLIC
+	USING (cid < 8);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_dave, regress_rls_alice
+	USING (true);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_exempt_user
+	WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 30 other objects
+NOTICE:  drop cascades to 32 other objects
 DETAIL:  drop cascades to function f_leak(text)
 drop cascades to table uaccount
 drop cascades to table category
@@ -4877,6 +5083,8 @@ drop cascades to table dep1
 drop cascades to table dep2
 drop cascades to table dob_t1
 drop cascades to table dob_t2
+drop cascades to table rls_tbl_1
+drop cascades to table rls_tbl_2
 DROP USER regress_rls_alice;
 DROP USER regress_rls_bob;
 DROP USER regress_rls_carol;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 2d1be543391..f9f5bd0ae7d 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2403,6 +2403,86 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1', false);
+SELECT pg_get_policy_ddl('tab1', NULL, false);
+SELECT pg_get_policy_ddl(NULL, NULL, false);
+
+-- Test -1 as table oid
+ SELECT pg_get_policy_ddl(-1, 'rls_p1', false);
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1', false);
+
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-22 18:49  Philip Alger <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Philip Alger @ 2025-10-22 18:49 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: jian he <[email protected]>; pgsql-hackers

>> The get_formatted_string function is needed. Instead of using multiple if
> statements for the pretty flag, it’s better to have a generic function.
> This will be useful if the pretty-format DDL implementation is accepted by
> the community, as it can be reused by other pg_get_<object>_ddl() DDL
> functions added in the future.
>
>>
>> in pg_get_triggerdef_worker, I found the below code pattern:
>>     /*
>>      * In non-pretty mode, always schema-qualify the target table name for
>>      * safety.  In pretty mode, schema-qualify only if not visible.
>>      */
>>     appendStringInfo(&buf, " ON %s ",
>>                      pretty ?
>>                      generate_relation_name(trigrec->tgrelid, NIL) :
>>                      generate_qualified_relation_name(trigrec->tgrelid));
>>
>> maybe we can apply it too while construct query string:
>> "CREATE POLICY %s ON %s",
>>
>
> In my opinion, the table name should always be schema-qualified, which I
> have addressed in the v4 patch. The implementation of the pretty flag
> here differs from pg_get_triggerdef_worker, which is used only for the
> target table name. In my patch, the pretty flag adds \t and \n to each
> different clause (example AS, FOR, USING ...)
>
>

I think that's where the confusion lies with adding `pretty` to the DDL
functions. The `pretty` flag set to `true` on other functions is used to
"not" show the schema name. But in the case of this function, it is used to
format the statement. I wonder if the word `format` or `pretty_format` is a
better name? `pretty` itself in the codebase isn't a very clear name
regardless.

-- 
Best,
Phil Alger


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-25 05:57  Akshay Joshi <[email protected]>
  parent: Philip Alger <[email protected]>
  0 siblings, 0 replies; 44+ messages in thread

From: Akshay Joshi @ 2025-10-25 05:57 UTC (permalink / raw)
  To: Philip Alger <[email protected]>; +Cc: jian he <[email protected]>; pgsql-hackers

On Thu, Oct 23, 2025 at 12:19 AM Philip Alger <[email protected]> wrote:

>
>
>
>>> The get_formatted_string function is needed. Instead of using multiple
>> if statements for the pretty flag, it’s better to have a generic
>> function. This will be useful if the pretty-format DDL implementation is
>> accepted by the community, as it can be reused by other
>> pg_get_<object>_ddl() DDL functions added in the future.
>>
>>>
>>> in pg_get_triggerdef_worker, I found the below code pattern:
>>>     /*
>>>      * In non-pretty mode, always schema-qualify the target table name
>>> for
>>>      * safety.  In pretty mode, schema-qualify only if not visible.
>>>      */
>>>     appendStringInfo(&buf, " ON %s ",
>>>                      pretty ?
>>>                      generate_relation_name(trigrec->tgrelid, NIL) :
>>>                      generate_qualified_relation_name(trigrec->tgrelid));
>>>
>>> maybe we can apply it too while construct query string:
>>> "CREATE POLICY %s ON %s",
>>>
>>
>> In my opinion, the table name should always be schema-qualified, which I
>> have addressed in the v4 patch. The implementation of the pretty flag
>> here differs from pg_get_triggerdef_worker, which is used only for the
>> target table name. In my patch, the pretty flag adds \t and \n to each
>> different clause (example AS, FOR, USING ...)
>>
>>
>
> I think that's where the confusion lies with adding `pretty` to the DDL
> functions. The `pretty` flag set to `true` on other functions is used to
> "not" show the schema name. But in the case of this function, it is used to
> format the statement. I wonder if the word `format` or `pretty_format` is a
> better name? `pretty` itself in the codebase isn't a very clear name
> regardless.
>
In my opinion, we should first decide whether we want the DDL statement to
be in a 'pretty' format or not. Personally, I believe it should be, since
parsing a one-line DDL statement can be quite complex and difficult for
users of this function. From a user’s perspective, having the entire DDL in
a single line makes it harder to read and understand. The flag allows users
to disable the pretty format by passing false.
If the community agrees on this approach, we can then think about choosing
a more appropriate word for it.

>
> --
> Best,
> Phil Alger
>


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-27 16:45  Mark Wong <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Mark Wong @ 2025-10-27 16:45 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Álvaro Herrera <[email protected]>; pgsql-hackers

Hi everyone,

On Thu, Oct 16, 2025 at 01:47:53PM +0530, Akshay Joshi wrote:
> 
> 
> On Wed, Oct 15, 2025 at 10:55 PM Álvaro Herrera <[email protected]> wrote:
> 
>     Hello,
> 
>     I have reviewed this patch before and provided a number of comments that
>     have been addressed by Akshay (so I encourage you to list my name and
>     this address in a Reviewed-by trailer line in the commit message).  One
>     thing I had not noticed is that while this function has a "pretty" flag,
>     it doesn't use it to pass anything to pg_get_expr_worker()'s prettyFlags
>     argument, and I think it should -- probably just
> 
>       prettyFlags = GET_PRETTY_FLAGS(pretty);
> 
>     same as pg_get_querydef() does.

Kinda sorta similar thought, I've noticed some existing functions like
pg_get_constraintdef make the "pretty" flag optional, so I'm wondering
if that scheme is also preferred here.

I've attached a small diff to the original
0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch to
illustrate the additional work to follow suit, if so desired.

Regards,
Mark
--
Mark Wong <[email protected]>
EDB https://enterprisedb.com

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 4b9c661c20b..72b836cd082 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3827,19 +3827,29 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
          <primary>pg_get_policy_ddl</primary>
         </indexterm>
         <function>pg_get_policy_ddl</function>
-        ( <parameter>table</parameter> <type>regclass</type>, <parameter>policy_name</parameter> <type>name</type>, <parameter>pretty</parameter> <type>boolean</type> )
+        ( <parameter>table</parameter> <type>regclass</type>, <parameter>policy_name</parameter> <type>name</type> <optional>, <parameter>pretty</parameter> <type>boolean</type> </optional> )
         <returnvalue>text</returnvalue>
        </para>
        <para>
-        Reconstructs the CREATE POLICY statement from the system catalogs for a specified table and policy name.
-        When the pretty flag is set to true, the function returns a well-formatted DDL statement.
-        The result is a comprehensive <command>CREATE POLICY</command> statement.
+        Reconstructs the <command>CREATE POLICY statement</command> from the
+        system catalogs for a specified table and policy name. The result is a
+        comprehensive <command>CREATE POLICY</command> statement.
        </para></entry>
       </row>
      </tbody>
     </tgroup>
    </table>
 
+  <para>
+   Most of the functions that reconstruct (decompile) database objects have an
+   optional <parameter>pretty</parameter> flag, which if
+   <literal>true</literal> causes the result to be
+   <quote>pretty-printed</quote>.  Pretty-printing adds whitespace for
+   legibility. Passing <literal>false</literal> for the
+   <parameter>pretty</parameter> parameter yields the same result as omitting
+   the parameter.
+  </para>
+
   </sect2>
 
   </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index c05e4786703..e6d21a1d00e 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -552,6 +552,7 @@ static void get_formatted_string(StringInfo buf,
 								 bool pretty,
 								 int noOfTabChars,
 								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char    *pg_get_policy_ddl_worker(Oid tableID, Name policyName, bool pretty);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13788,12 +13789,41 @@ get_formatted_string(StringInfo buf, bool pretty, int noOfTabChars, const char *
  * policyName - Name of the policy for which to generate the DDL.
  * pretty - If true, format the DDL with indentation and line breaks.
  */
+
 Datum
 pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	char	   *res;
+
+	res = pg_get_policy_ddl_worker(tableID, policyName, false);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+Datum
+pg_get_policy_ddl_ext(PG_FUNCTION_ARGS)
 {
 	Oid			tableID = PG_GETARG_OID(0);
 	Name		policyName = PG_GETARG_NAME(1);
 	bool		pretty = PG_GETARG_BOOL(2);
+	char	   *res;
+
+	res = pg_get_policy_ddl_worker(tableID, policyName, pretty);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_policy_ddl_worker(Oid tableID, Name policyName, bool pretty)
+{
 	bool		attrIsNull;
 	int			prettyFlags;
 	Datum		valueDatum;
@@ -13807,7 +13837,7 @@ pg_get_policy_ddl(PG_FUNCTION_ARGS)
 
 	/* Validate that the relation exists */
 	if (!OidIsValid(tableID) || get_rel_name(tableID) == NULL)
-		PG_RETURN_NULL();
+		return NULL;
 
 	initStringInfo(&buf);
 
@@ -13935,5 +13965,5 @@ pg_get_policy_ddl(PG_FUNCTION_ARGS)
 	systable_endscan(sscan);
 	table_close(pgPolicyRel, AccessShareLock);
 
-	PG_RETURN_TEXT_P(string_to_text(buf.data));
+	return buf.data;
 }
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 536c5a857da..3bfaf34d535 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4023,7 +4023,10 @@
   prosrc => 'pg_get_function_sqlbody' },
 { oid => '8811', descr => 'get CREATE statement for policy',
   proname => 'pg_get_policy_ddl', prorettype => 'text',
-  proargtypes => 'regclass name bool', prosrc => 'pg_get_policy_ddl' },
+  proargtypes => 'regclass name', prosrc => 'pg_get_policy_ddl' },
+{ oid => '8812', descr => 'get CREATE statement for policy with pretty-print option',
+  proname => 'pg_get_policy_ddl', prorettype => 'text',
+  proargtypes => 'regclass name bool', prosrc => 'pg_get_policy_ddl_ext' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',


Attachments:

  [text/plain] for-optional-pretty.diff (4.6K, ../../aP-hllbRdgqbmB8L@ltdrgnflg2/2-for-optional-pretty.diff)
  download | inline diff:
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 4b9c661c20b..72b836cd082 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3827,19 +3827,29 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
          <primary>pg_get_policy_ddl</primary>
         </indexterm>
         <function>pg_get_policy_ddl</function>
-        ( <parameter>table</parameter> <type>regclass</type>, <parameter>policy_name</parameter> <type>name</type>, <parameter>pretty</parameter> <type>boolean</type> )
+        ( <parameter>table</parameter> <type>regclass</type>, <parameter>policy_name</parameter> <type>name</type> <optional>, <parameter>pretty</parameter> <type>boolean</type> </optional> )
         <returnvalue>text</returnvalue>
        </para>
        <para>
-        Reconstructs the CREATE POLICY statement from the system catalogs for a specified table and policy name.
-        When the pretty flag is set to true, the function returns a well-formatted DDL statement.
-        The result is a comprehensive <command>CREATE POLICY</command> statement.
+        Reconstructs the <command>CREATE POLICY statement</command> from the
+        system catalogs for a specified table and policy name. The result is a
+        comprehensive <command>CREATE POLICY</command> statement.
        </para></entry>
       </row>
      </tbody>
     </tgroup>
    </table>
 
+  <para>
+   Most of the functions that reconstruct (decompile) database objects have an
+   optional <parameter>pretty</parameter> flag, which if
+   <literal>true</literal> causes the result to be
+   <quote>pretty-printed</quote>.  Pretty-printing adds whitespace for
+   legibility. Passing <literal>false</literal> for the
+   <parameter>pretty</parameter> parameter yields the same result as omitting
+   the parameter.
+  </para>
+
   </sect2>
 
   </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index c05e4786703..e6d21a1d00e 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -552,6 +552,7 @@ static void get_formatted_string(StringInfo buf,
 								 bool pretty,
 								 int noOfTabChars,
 								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char    *pg_get_policy_ddl_worker(Oid tableID, Name policyName, bool pretty);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13788,12 +13789,41 @@ get_formatted_string(StringInfo buf, bool pretty, int noOfTabChars, const char *
  * policyName - Name of the policy for which to generate the DDL.
  * pretty - If true, format the DDL with indentation and line breaks.
  */
+
 Datum
 pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	char	   *res;
+
+	res = pg_get_policy_ddl_worker(tableID, policyName, false);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+Datum
+pg_get_policy_ddl_ext(PG_FUNCTION_ARGS)
 {
 	Oid			tableID = PG_GETARG_OID(0);
 	Name		policyName = PG_GETARG_NAME(1);
 	bool		pretty = PG_GETARG_BOOL(2);
+	char	   *res;
+
+	res = pg_get_policy_ddl_worker(tableID, policyName, pretty);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_policy_ddl_worker(Oid tableID, Name policyName, bool pretty)
+{
 	bool		attrIsNull;
 	int			prettyFlags;
 	Datum		valueDatum;
@@ -13807,7 +13837,7 @@ pg_get_policy_ddl(PG_FUNCTION_ARGS)
 
 	/* Validate that the relation exists */
 	if (!OidIsValid(tableID) || get_rel_name(tableID) == NULL)
-		PG_RETURN_NULL();
+		return NULL;
 
 	initStringInfo(&buf);
 
@@ -13935,5 +13965,5 @@ pg_get_policy_ddl(PG_FUNCTION_ARGS)
 	systable_endscan(sscan);
 	table_close(pgPolicyRel, AccessShareLock);
 
-	PG_RETURN_TEXT_P(string_to_text(buf.data));
+	return buf.data;
 }
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 536c5a857da..3bfaf34d535 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4023,7 +4023,10 @@
   prosrc => 'pg_get_function_sqlbody' },
 { oid => '8811', descr => 'get CREATE statement for policy',
   proname => 'pg_get_policy_ddl', prorettype => 'text',
-  proargtypes => 'regclass name bool', prosrc => 'pg_get_policy_ddl' },
+  proargtypes => 'regclass name', prosrc => 'pg_get_policy_ddl' },
+{ oid => '8812', descr => 'get CREATE statement for policy with pretty-print option',
+  proname => 'pg_get_policy_ddl', prorettype => 'text',
+  proargtypes => 'regclass name bool', prosrc => 'pg_get_policy_ddl_ext' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-10-28 09:38  Akshay Joshi <[email protected]>
  parent: Mark Wong <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2025-10-28 09:38 UTC (permalink / raw)
  To: Mark Wong <[email protected]>; +Cc: Álvaro Herrera <[email protected]>; pgsql-hackers

Thanks, Mark, for your review comments and the updated patch.

I’ve incorporated your changes and prepared a combined v5 patch. The v5
patch is attached for further review.

On Mon, Oct 27, 2025 at 10:15 PM Mark Wong <[email protected]> wrote:

> Hi everyone,
>
> On Thu, Oct 16, 2025 at 01:47:53PM +0530, Akshay Joshi wrote:
> >
> >
> > On Wed, Oct 15, 2025 at 10:55 PM Álvaro Herrera <[email protected]>
> wrote:
> >
> >     Hello,
> >
> >     I have reviewed this patch before and provided a number of comments
> that
> >     have been addressed by Akshay (so I encourage you to list my name and
> >     this address in a Reviewed-by trailer line in the commit message).
> One
> >     thing I had not noticed is that while this function has a "pretty"
> flag,
> >     it doesn't use it to pass anything to pg_get_expr_worker()'s
> prettyFlags
> >     argument, and I think it should -- probably just
> >
> >       prettyFlags = GET_PRETTY_FLAGS(pretty);
> >
> >     same as pg_get_querydef() does.
>
> Kinda sorta similar thought, I've noticed some existing functions like
> pg_get_constraintdef make the "pretty" flag optional, so I'm wondering
> if that scheme is also preferred here.
>
> I've attached a small diff to the original
> 0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch to
> illustrate the additional work to follow suit, if so desired.
>
> Regards,
> Mark
> --
> Mark Wong <[email protected]>
> EDB https://enterprisedb.com
>


Attachments:

  [application/octet-stream] v5-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch (25.7K, ../../CANxoLDc8UnFuKA2RX6UR_KZWRH6itmrhXK7hoFyF=5kCyfFOGA@mail.gmail.com/3-v5-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From 0daa0f68ac03c63fe2a5a9ac0909c324d5937f5f Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 10 Oct 2025 15:46:13 +0530
Subject: [PATCH v5] Add pg_get_policy_ddl() function to reconstruct CREATE
 POLICY statements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduces a new system function:
pg_get_policy_ddl(regclass table, name policy_name, bool pretty),
which reconstructs the CREATE POLICY statement for the specified policy.

Usage examples:
SELECT pg_get_policy_ddl('rls_table', 'pol1'); -- non-pretty formatted DDL
SELECT pg_get_policy_ddl('rls_table', 'pol1', true);  -- pretty formatted DDL

Reference: PG-163
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  55 ++++++
 src/backend/commands/policy.c             |  27 +++
 src/backend/utils/adt/ruleutils.c         | 228 ++++++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   6 +
 src/include/commands/policy.h             |   2 +
 src/test/regress/expected/rowsecurity.out | 210 +++++++++++++++++++-
 src/test/regress/sql/rowsecurity.sql      |  80 ++++++++
 7 files changed, 607 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c..2210a49a478 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,59 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </para>
+
+   <table id="functions-get-object-ddl-table">
+    <title>Get Object DDL Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>, <parameter>policy_name</parameter> <type>name</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement from the
+        system catalogs for a specified table and policy name. The result is a
+        comprehensive <command>CREATE POLICY</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+   Most of the functions that reconstruct (decompile) database objects have an
+   optional <parameter>pretty</parameter> flag, which if
+   <literal>true</literal> causes the result to be
+   <quote>pretty-printed</quote>. Pretty-printing adds tab character and new
+   line character for legibility. Passing <literal>false</literal> for the
+   <parameter>pretty</parameter> parameter yields the same result as omitting
+   the parameter.
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 83056960fe4..1abe0c44353 100644
--- a/src/backend/commands/policy.c
+++ b/src/backend/commands/policy.c
@@ -128,6 +128,33 @@ parse_policy_command(const char *cmd_name)
 	return polcmd;
 }
 
+/*
+ * get_policy_cmd_name -
+ *	 helper function to convert char representation to full command strings.
+ *
+ * cmd -  Valid values are '*', 'r', 'a', 'w' and 'd'.
+ *
+ */
+char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command");
+	}
+}
+
 /*
  * policy_role_list_to_array
  *	 helper function to convert a list of RoleSpecs to an array of
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 79ec136231b..b773c7725a4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -33,12 +33,14 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
+#include "commands/policy.h"
 #include "common/keywords.h"
 #include "executor/spi.h"
 #include "funcapi.h"
@@ -546,6 +548,11 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
 										  deparse_context *context,
 										  bool showimplicit,
 										  bool needcomma);
+static void get_formatted_string(StringInfo buf,
+								 bool pretty,
+								 int noOfTabChars,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_policy_ddl_worker(Oid tableID, Name policyName, bool pretty);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13738,3 +13745,224 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * pretty - If pretty is true, the output includes tabs (\t) and newlines (\n).
+ * noOfTabChars - indent with specified no of tabs.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, bool pretty, int noOfTabChars, const char *fmt,...)
+{
+	va_list		args;
+
+	if (pretty)
+	{
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+
+	/* If pretty mode, append newline at the end */
+	if (pretty)
+		appendStringInfoChar(buf, '\n');
+}
+
+/*
+ * pg_get_policy_ddl
+ *
+ * Generate a CREATE POLICY statement for the specified policy.
+ *
+ * tableID - Table ID of the policy.
+ * policyName - Name of the policy for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	char	   *res;
+
+	res = pg_get_policy_ddl_worker(tableID, policyName, false);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+Datum
+pg_get_policy_ddl_ext(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	bool		pretty = PG_GETARG_BOOL(2);
+	char	   *res;
+
+	res = pg_get_policy_ddl_worker(tableID, policyName, pretty);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_policy_ddl_worker(Oid tableID, Name policyName, bool pretty)
+{
+	bool		attrIsNull;
+	int			prettyFlags;
+	Datum		valueDatum;
+	HeapTuple	tuplePolicy;
+	Relation	pgPolicyRel;
+	char	   *targetTable;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	Form_pg_policy policyForm;
+	StringInfoData buf;
+
+	/* Validate that the relation exists */
+	if (!OidIsValid(tableID) || get_rel_name(tableID) == NULL)
+		return NULL;
+
+	initStringInfo(&buf);
+
+	targetTable = generate_qualified_relation_name(tableID);
+	/* Find policy to begin scan */
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(NameStr(*policyName)));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	/* Check that the policy is found, raise an error if not. */
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						NameStr(*policyName),
+						targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	/* Build the CREATE POLICY statement */
+	get_formatted_string(&buf, pretty, 0, "CREATE POLICY %s ON %s",
+						 quote_identifier(NameStr(*policyName)),
+						 targetTable);
+
+	/* Check the type is PERMISSIVE or RESTRICTIVE */
+	get_formatted_string(&buf, pretty, 1,
+						 policyForm->polpermissive ? "AS PERMISSIVE" : "AS RESTRICTIVE");
+
+	/* Check command to which the policy applies */
+	get_formatted_string(&buf, pretty, 1, "FOR %s",
+						 get_policy_cmd_name(policyForm->polcmd));
+
+	/* Check if the policy has a TO list */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (i > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, rolename);
+			}
+		}
+
+		if (role_names.len > 0)
+			get_formatted_string(&buf, pretty, 1, "TO %s", role_names.data);
+		else
+
+			/*
+			 * When no specific role is provided, generate the TO clause with
+			 * the PUBLIC role.
+			 */
+			get_formatted_string(&buf, pretty, 1, "TO PUBLIC");
+	}
+
+	prettyFlags = GET_PRETTY_FLAGS(pretty);
+	/* Check if the policy has a USING expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *usingExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, pretty, 1, "USING (%s)",
+							 text_to_cstring(usingExpression));
+	}
+
+	/* Check if the policy has a WITH CHECK expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *checkExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, pretty, 1, "WITH CHECK (%s)",
+							 text_to_cstring(checkExpression));
+	}
+
+	/* Replace '\n' with ';' if newline at the end */
+	if (buf.len > 0 && buf.data[buf.len - 1] == '\n')
+		buf.data[buf.len - 1] = ';';
+	else
+		appendStringInfoChar(&buf, ';');
+
+	/* Clean up. */
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9121a382f76..69683fb37f7 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,12 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '8811', descr => 'get CREATE statement for policy',
+  proname => 'pg_get_policy_ddl', prorettype => 'text',
+  proargtypes => 'regclass name', prosrc => 'pg_get_policy_ddl' },
+{ oid => '8812', descr => 'get CREATE statement for policy with pretty option',
+  proname => 'pg_get_policy_ddl', prorettype => 'text',
+  proargtypes => 'regclass name bool', prosrc => 'pg_get_policy_ddl_ext' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h
index f06aa1df439..40e45b738f4 100644
--- a/src/include/commands/policy.h
+++ b/src/include/commands/policy.h
@@ -35,4 +35,6 @@ extern ObjectAddress rename_policy(RenameStmt *stmt);
 
 extern bool relation_has_policies(Relation rel);
 
+extern char *get_policy_cmd_name(char cmd);
+
 #endif							/* POLICY_H */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index c958ef4d70a..abc3e84c5de 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -5101,11 +5101,217 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1', false);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+SELECT pg_get_policy_ddl('tab1', NULL, false);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', NULL, false);
+                                 ^
+SELECT pg_get_policy_ddl(NULL, NULL, false);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Test -1 as table oid
+ SELECT pg_get_policy_ddl(-1, 'rls_p1', false);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+                                 ^
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1', false);
+ERROR:  policy "pol1" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', false);
+                                                        pg_get_policy_ddl                                                         
+----------------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO PUBLIC USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                                                               +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', false);
+                                                      pg_get_policy_ddl                                                       
+------------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE FOR ALL TO PUBLIC USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', false);
+                                                    pg_get_policy_ddl                                                    
+-------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO PUBLIC USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', false);
+                                                 pg_get_policy_ddl                                                 
+-------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR SELECT TO PUBLIC USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', false);
+                                                   pg_get_policy_ddl                                                    
+------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR INSERT TO PUBLIC WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+                                                 pg_get_policy_ddl                                                 
+-------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR UPDATE TO PUBLIC USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+                                              pg_get_policy_ddl                                              
+-------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR DELETE TO PUBLIC USING ((cid < 8));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+                                                        pg_get_policy_ddl                                                         
+----------------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+                                                                 pg_get_policy_ddl                                                                  
+----------------------------------------------------------------------------------------------------------------------------------------------------
+  CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_exempt_user WITH CHECK ((cid = ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2)));
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO PUBLIC
+	USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+	AS RESTRICTIVE
+	FOR ALL
+	TO PUBLIC
+	USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO PUBLIC
+	USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR SELECT
+	TO PUBLIC
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR INSERT
+	TO PUBLIC
+	WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR UPDATE
+	TO PUBLIC
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR DELETE
+	TO PUBLIC
+	USING (cid < 8);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_dave, regress_rls_alice
+	USING (true);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_exempt_user
+	WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 30 other objects
+NOTICE:  drop cascades to 32 other objects
 DETAIL:  drop cascades to function f_leak(text)
 drop cascades to table uaccount
 drop cascades to table category
@@ -5136,6 +5342,8 @@ drop cascades to table dep1
 drop cascades to table dep2
 drop cascades to table dob_t1
 drop cascades to table dob_t2
+drop cascades to table rls_tbl_1
+drop cascades to table rls_tbl_2
 DROP USER regress_rls_alice;
 DROP USER regress_rls_bob;
 DROP USER regress_rls_carol;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 5d923c5ca3b..22ff813ea24 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2542,6 +2542,86 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1', false);
+SELECT pg_get_policy_ddl('tab1', NULL, false);
+SELECT pg_get_policy_ddl(NULL, NULL, false);
+
+-- Test -1 as table oid
+ SELECT pg_get_policy_ddl(-1, 'rls_p1', false);
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1', false);
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1', false);
+
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-11-03 11:47  Akshay Joshi <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2025-11-03 11:47 UTC (permalink / raw)
  To: Mark Wong <[email protected]>; +Cc: Álvaro Herrera <[email protected]>; pgsql-hackers

Hi Hackers,

Added a new #define GET_DDL_PRETTY_FLAGS because the existing #define
GET_PRETTY_FLAGS is not suitable for formatting reconstructed DDLs. The
existing #define GET_PRETTY_FLAGS always indents the code, regardless of
whether the flag is set to true or false, which is not the desired behavior
for pg_get_<object>_ddl functions.

Updated the logic of the get_formatted_string function based on Tim
Waizenegger’s suggestion.

I am attaching the new v6 patch, which is ready for review.

On Tue, Oct 28, 2025 at 3:08 PM Akshay Joshi <[email protected]>
wrote:

> Thanks, Mark, for your review comments and the updated patch.
>
> I’ve incorporated your changes and prepared a combined v5 patch. The v5
> patch is attached for further review.
>
> On Mon, Oct 27, 2025 at 10:15 PM Mark Wong <[email protected]> wrote:
>
>> Hi everyone,
>>
>> On Thu, Oct 16, 2025 at 01:47:53PM +0530, Akshay Joshi wrote:
>> >
>> >
>> > On Wed, Oct 15, 2025 at 10:55 PM Álvaro Herrera <[email protected]>
>> wrote:
>> >
>> >     Hello,
>> >
>> >     I have reviewed this patch before and provided a number of comments
>> that
>> >     have been addressed by Akshay (so I encourage you to list my name
>> and
>> >     this address in a Reviewed-by trailer line in the commit message).
>> One
>> >     thing I had not noticed is that while this function has a "pretty"
>> flag,
>> >     it doesn't use it to pass anything to pg_get_expr_worker()'s
>> prettyFlags
>> >     argument, and I think it should -- probably just
>> >
>> >       prettyFlags = GET_PRETTY_FLAGS(pretty);
>> >
>> >     same as pg_get_querydef() does.
>>
>> Kinda sorta similar thought, I've noticed some existing functions like
>> pg_get_constraintdef make the "pretty" flag optional, so I'm wondering
>> if that scheme is also preferred here.
>>
>> I've attached a small diff to the original
>> 0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch to
>> illustrate the additional work to follow suit, if so desired.
>>
>> Regards,
>> Mark
>> --
>> Mark Wong <[email protected]>
>> EDB https://enterprisedb.com
>>
>


Attachments:

  [application/octet-stream] v6-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch (25.7K, ../../CANxoLDfyk_+h91FNq5VNemXTBpES0aLPtLp2myTyWgquUHSQ3A@mail.gmail.com/3-v6-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From 0d505dc15d89ba59a845d4057a67096d4e5f4284 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 10 Oct 2025 15:46:13 +0530
Subject: [PATCH v6] Add pg_get_policy_ddl() function to reconstruct CREATE
 POLICY statements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduces a new system function:
pg_get_policy_ddl(regclass table, name policy_name, bool pretty),
which reconstructs the CREATE POLICY statement for the specified policy.

Usage examples:
SELECT pg_get_policy_ddl('rls_table', 'pol1'); -- non-pretty formatted DDL
SELECT pg_get_policy_ddl('rls_table', 'pol1', true);  -- pretty formatted DDL

Reference: PG-163
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  55 ++++++
 src/backend/commands/policy.c             |  27 +++
 src/backend/utils/adt/ruleutils.c         | 227 ++++++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   6 +
 src/include/commands/policy.h             |   2 +
 src/test/regress/expected/rowsecurity.out | 207 +++++++++++++++++++-
 src/test/regress/sql/rowsecurity.sql      |  80 ++++++++
 7 files changed, 603 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94c..2210a49a478 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,59 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </para>
+
+   <table id="functions-get-object-ddl-table">
+    <title>Get Object DDL Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>, <parameter>policy_name</parameter> <type>name</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement from the
+        system catalogs for a specified table and policy name. The result is a
+        comprehensive <command>CREATE POLICY</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+   Most of the functions that reconstruct (decompile) database objects have an
+   optional <parameter>pretty</parameter> flag, which if
+   <literal>true</literal> causes the result to be
+   <quote>pretty-printed</quote>. Pretty-printing adds tab character and new
+   line character for legibility. Passing <literal>false</literal> for the
+   <parameter>pretty</parameter> parameter yields the same result as omitting
+   the parameter.
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 83056960fe4..1abe0c44353 100644
--- a/src/backend/commands/policy.c
+++ b/src/backend/commands/policy.c
@@ -128,6 +128,33 @@ parse_policy_command(const char *cmd_name)
 	return polcmd;
 }
 
+/*
+ * get_policy_cmd_name -
+ *	 helper function to convert char representation to full command strings.
+ *
+ * cmd -  Valid values are '*', 'r', 'a', 'w' and 'd'.
+ *
+ */
+char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command");
+	}
+}
+
 /*
  * policy_role_list_to_array
  *	 helper function to convert a list of RoleSpecs to an array of
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 79ec136231b..5bf8a1e6467 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -33,12 +33,14 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
+#include "commands/policy.h"
 #include "common/keywords.h"
 #include "executor/spi.h"
 #include "funcapi.h"
@@ -94,6 +96,10 @@
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#define GET_DDL_PRETTY_FLAGS(pretty) \
+	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
+	 : 0)
+
 /* Default line length for pretty-print wrapping: 0 means wrap always */
 #define WRAP_COLUMN_DEFAULT		0
 
@@ -546,6 +552,12 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
 										  deparse_context *context,
 										  bool showimplicit,
 										  bool needcomma);
+static void get_formatted_string(StringInfo buf,
+								 int prettyFlags,
+								 int noOfTabChars,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_policy_ddl_worker(Oid tableID, Name policyName,
+									  int prettyFlags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13738,3 +13750,218 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags the output includes tabs (\t) and
+ *               newlines (\n).
+ * noOfTabChars - indent with specified no of tabs.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int noOfTabChars, const char *fmt,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+/*
+ * pg_get_policy_ddl
+ *
+ * Generate a CREATE POLICY statement for the specified policy.
+ *
+ * tableID - Table ID of the policy.
+ * policyName - Name of the policy for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	char	   *res;
+
+	res = pg_get_policy_ddl_worker(tableID, policyName, false);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+Datum
+pg_get_policy_ddl_ext(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	bool		pretty = PG_GETARG_BOOL(2);
+	int			prettyFlags;
+	char	   *res;
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+	res = pg_get_policy_ddl_worker(tableID, policyName, prettyFlags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_policy_ddl_worker(Oid tableID, Name policyName, int prettyFlags)
+{
+	bool		attrIsNull;
+	Datum		valueDatum;
+	HeapTuple	tuplePolicy;
+	Relation	pgPolicyRel;
+	char	   *targetTable;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	Form_pg_policy policyForm;
+	StringInfoData buf;
+
+	/* Validate that the relation exists */
+	if (!OidIsValid(tableID) || get_rel_name(tableID) == NULL)
+		return NULL;
+
+	initStringInfo(&buf);
+
+	targetTable = generate_qualified_relation_name(tableID);
+	/* Find policy to begin scan */
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(NameStr(*policyName)));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	/* Check that the policy is found, raise an error if not. */
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						NameStr(*policyName),
+						targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	/* Build the CREATE POLICY statement */
+	appendStringInfo(&buf, "CREATE POLICY %s ON %s",
+					 quote_identifier(NameStr(*policyName)),
+					 targetTable);
+
+	/* Check the type is PERMISSIVE or RESTRICTIVE */
+	get_formatted_string(&buf, prettyFlags, 1,
+						 policyForm->polpermissive ? "AS PERMISSIVE" : "AS RESTRICTIVE");
+
+	/* Check command to which the policy applies */
+	get_formatted_string(&buf, prettyFlags, 1, "FOR %s",
+						 get_policy_cmd_name(policyForm->polcmd));
+
+	/* Check if the policy has a TO list */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (i > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, rolename);
+			}
+		}
+
+		if (role_names.len > 0)
+			get_formatted_string(&buf, prettyFlags, 1, "TO %s", role_names.data);
+		else
+
+			/*
+			 * When no specific role is provided, generate the TO clause with
+			 * the PUBLIC role.
+			 */
+			get_formatted_string(&buf, prettyFlags, 1, "TO PUBLIC");
+	}
+
+	/* Check if the policy has a USING expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *usingExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, prettyFlags, 1, "USING (%s)",
+							 text_to_cstring(usingExpression));
+	}
+
+	/* Check if the policy has a WITH CHECK expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *checkExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, prettyFlags, 1, "WITH CHECK (%s)",
+							 text_to_cstring(checkExpression));
+	}
+
+	appendStringInfoChar(&buf, ';');
+
+	/* Clean up. */
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9121a382f76..69683fb37f7 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,12 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '8811', descr => 'get CREATE statement for policy',
+  proname => 'pg_get_policy_ddl', prorettype => 'text',
+  proargtypes => 'regclass name', prosrc => 'pg_get_policy_ddl' },
+{ oid => '8812', descr => 'get CREATE statement for policy with pretty option',
+  proname => 'pg_get_policy_ddl', prorettype => 'text',
+  proargtypes => 'regclass name bool', prosrc => 'pg_get_policy_ddl_ext' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h
index f06aa1df439..40e45b738f4 100644
--- a/src/include/commands/policy.h
+++ b/src/include/commands/policy.h
@@ -35,4 +35,6 @@ extern ObjectAddress rename_policy(RenameStmt *stmt);
 
 extern bool relation_has_policies(Relation rel);
 
+extern char *get_policy_cmd_name(char cmd);
+
 #endif							/* POLICY_H */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index c958ef4d70a..4ebe32711cd 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -5101,11 +5101,214 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1');
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+SELECT pg_get_policy_ddl('tab1', NULL);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', NULL);
+                                 ^
+SELECT pg_get_policy_ddl(NULL, NULL);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Test -1 as table oid
+ SELECT pg_get_policy_ddl(-1, 'rls_p1');
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1');
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', 'rls_p1');
+                                 ^
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1');
+ERROR:  policy "pol1" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+                                                                                     pg_get_policy_ddl                                                                                     
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO PUBLIC USING ((dlevel <= (SELECT rls_tbl_2.seclv FROM rls_tbl_2 WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+                                                      pg_get_policy_ddl                                                      
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE FOR ALL TO PUBLIC USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+                                                   pg_get_policy_ddl                                                    
+------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO PUBLIC USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+                                                pg_get_policy_ddl                                                 
+------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR SELECT TO PUBLIC USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+                                                   pg_get_policy_ddl                                                   
+-----------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR INSERT TO PUBLIC WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+                                                pg_get_policy_ddl                                                 
+------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR UPDATE TO PUBLIC USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+                                             pg_get_policy_ddl                                              
+------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR DELETE TO PUBLIC USING ((cid < 8));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+                                                        pg_get_policy_ddl                                                        
+---------------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+                                                                          pg_get_policy_ddl                                                                          
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_exempt_user WITH CHECK ((cid = (SELECT rls_tbl_2.seclv FROM rls_tbl_2)));
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO PUBLIC
+	USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+	AS RESTRICTIVE
+	FOR ALL
+	TO PUBLIC
+	USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO PUBLIC
+	USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR SELECT
+	TO PUBLIC
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR INSERT
+	TO PUBLIC
+	WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR UPDATE
+	TO PUBLIC
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR DELETE
+	TO PUBLIC
+	USING (cid < 8);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_dave, regress_rls_alice
+	USING (true);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_exempt_user
+	WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 30 other objects
+NOTICE:  drop cascades to 32 other objects
 DETAIL:  drop cascades to function f_leak(text)
 drop cascades to table uaccount
 drop cascades to table category
@@ -5136,6 +5339,8 @@ drop cascades to table dep1
 drop cascades to table dep2
 drop cascades to table dob_t1
 drop cascades to table dob_t2
+drop cascades to table rls_tbl_1
+drop cascades to table rls_tbl_2
 DROP USER regress_rls_alice;
 DROP USER regress_rls_bob;
 DROP USER regress_rls_carol;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 5d923c5ca3b..b90f5309578 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2542,6 +2542,86 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1');
+SELECT pg_get_policy_ddl('tab1', NULL);
+SELECT pg_get_policy_ddl(NULL, NULL);
+
+-- Test -1 as table oid
+ SELECT pg_get_policy_ddl(-1, 'rls_p1');
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1');
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1');
+
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-11-07 12:20  Akshay Joshi <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2025-11-07 12:20 UTC (permalink / raw)
  To: Mark Wong <[email protected]>; +Cc: Álvaro Herrera <[email protected]>; pgsql-hackers

Hi Hackers,

Make the pretty flag a default parameter by adding CREATE OR REPLACE
FUNCTION in system_functions.sql.
Attached is the v7 patch, which is ready for review.

On Mon, Nov 3, 2025 at 5:17 PM Akshay Joshi <[email protected]>
wrote:

> Hi Hackers,
>
> Added a new #define GET_DDL_PRETTY_FLAGS because the existing #define
> GET_PRETTY_FLAGS is not suitable for formatting reconstructed DDLs. The
> existing #define GET_PRETTY_FLAGS always indents the code, regardless of
> whether the flag is set to true or false, which is not the desired
> behavior for pg_get_<object>_ddl functions.
>
> Updated the logic of the get_formatted_string function based on Tim
> Waizenegger’s suggestion.
>
> I am attaching the new v6 patch, which is ready for review.
>
> On Tue, Oct 28, 2025 at 3:08 PM Akshay Joshi <
> [email protected]> wrote:
>
>> Thanks, Mark, for your review comments and the updated patch.
>>
>> I’ve incorporated your changes and prepared a combined v5 patch. The v5
>> patch is attached for further review.
>>
>> On Mon, Oct 27, 2025 at 10:15 PM Mark Wong <[email protected]> wrote:
>>
>>> Hi everyone,
>>>
>>> On Thu, Oct 16, 2025 at 01:47:53PM +0530, Akshay Joshi wrote:
>>> >
>>> >
>>> > On Wed, Oct 15, 2025 at 10:55 PM Álvaro Herrera <[email protected]>
>>> wrote:
>>> >
>>> >     Hello,
>>> >
>>> >     I have reviewed this patch before and provided a number of
>>> comments that
>>> >     have been addressed by Akshay (so I encourage you to list my name
>>> and
>>> >     this address in a Reviewed-by trailer line in the commit
>>> message).  One
>>> >     thing I had not noticed is that while this function has a "pretty"
>>> flag,
>>> >     it doesn't use it to pass anything to pg_get_expr_worker()'s
>>> prettyFlags
>>> >     argument, and I think it should -- probably just
>>> >
>>> >       prettyFlags = GET_PRETTY_FLAGS(pretty);
>>> >
>>> >     same as pg_get_querydef() does.
>>>
>>> Kinda sorta similar thought, I've noticed some existing functions like
>>> pg_get_constraintdef make the "pretty" flag optional, so I'm wondering
>>> if that scheme is also preferred here.
>>>
>>> I've attached a small diff to the original
>>> 0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch to
>>> illustrate the additional work to follow suit, if so desired.
>>>
>>> Regards,
>>> Mark
>>> --
>>> Mark Wong <[email protected]>
>>> EDB https://enterprisedb.com
>>>
>>


Attachments:

  [application/octet-stream] v7-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch (25.9K, ../../CANxoLDfw=ERY89RR08s+qXwgUQvLCFF0pVG_e0fDT14rwZsWRg@mail.gmail.com/3-v7-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From 8ba841c91127f9774907586f94f732321b867fb2 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 10 Oct 2025 15:46:13 +0530
Subject: [PATCH v7] Add pg_get_policy_ddl() function to reconstruct CREATE
 POLICY statements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduces a new system function:
pg_get_policy_ddl(regclass table, name policy_name, bool pretty),
which reconstructs the CREATE POLICY statement for the specified policy.

Usage examples:
SELECT pg_get_policy_ddl('rls_table', 'pol1'); -- non-pretty formatted DDL
SELECT pg_get_policy_ddl('rls_table', 'pol1', true);  -- pretty formatted DDL

Reference: PG-163
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  55 ++++++
 src/backend/catalog/system_functions.sql  |   6 +
 src/backend/commands/policy.c             |  27 +++
 src/backend/utils/adt/ruleutils.c         | 212 ++++++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   3 +
 src/include/commands/policy.h             |   2 +
 src/test/regress/expected/rowsecurity.out | 207 ++++++++++++++++++++-
 src/test/regress/sql/rowsecurity.sql      |  80 ++++++++
 8 files changed, 591 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..edfd9381be0 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,59 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </para>
+
+   <table id="functions-get-object-ddl-table">
+    <title>Get Object DDL Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>, <parameter>policy_name</parameter> <type>name</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement from the
+        system catalogs for a specified table and policy name. The result is a
+        comprehensive <command>CREATE POLICY</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+   Most of the functions that reconstruct (decompile) database objects have an
+   optional <parameter>pretty</parameter> flag, which if
+   <literal>true</literal> causes the result to be
+   <quote>pretty-printed</quote>. Pretty-printing adds tab character and new
+   line character for legibility. Passing <literal>false</literal> for the
+   <parameter>pretty</parameter> parameter yields the same result as omitting
+   the parameter.
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..a5e22374668 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,12 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+  pg_get_policy_ddl(tableID regclass, policyName name, pretty bool DEFAULT false)
+RETURNS text
+LANGUAGE INTERNAL
+AS 'pg_get_policy_ddl';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 83056960fe4..1abe0c44353 100644
--- a/src/backend/commands/policy.c
+++ b/src/backend/commands/policy.c
@@ -128,6 +128,33 @@ parse_policy_command(const char *cmd_name)
 	return polcmd;
 }
 
+/*
+ * get_policy_cmd_name -
+ *	 helper function to convert char representation to full command strings.
+ *
+ * cmd -  Valid values are '*', 'r', 'a', 'w' and 'd'.
+ *
+ */
+char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command");
+	}
+}
+
 /*
  * policy_role_list_to_array
  *	 helper function to convert a list of RoleSpecs to an array of
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..d8e145f9e4c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -33,12 +33,14 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
+#include "commands/policy.h"
 #include "common/keywords.h"
 #include "executor/spi.h"
 #include "funcapi.h"
@@ -94,6 +96,10 @@
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#define GET_DDL_PRETTY_FLAGS(pretty) \
+	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
+	 : 0)
+
 /* Default line length for pretty-print wrapping: 0 means wrap always */
 #define WRAP_COLUMN_DEFAULT		0
 
@@ -546,6 +552,12 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
 										  deparse_context *context,
 										  bool showimplicit,
 										  bool needcomma);
+static void get_formatted_string(StringInfo buf,
+								 int prettyFlags,
+								 int noOfTabChars,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_policy_ddl_worker(Oid tableID, Name policyName,
+									  int prettyFlags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13743,3 +13755,203 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags the output includes tabs (\t) and
+ *               newlines (\n).
+ * noOfTabChars - indent with specified no of tabs.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int noOfTabChars, const char *fmt,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+/*
+ * pg_get_policy_ddl
+ *
+ * Generate a CREATE POLICY statement for the specified policy.
+ *
+ * tableID - Table ID of the policy.
+ * policyName - Name of the policy for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	bool		pretty = PG_GETARG_BOOL(2);
+	int			prettyFlags;
+	char	   *res;
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+	res = pg_get_policy_ddl_worker(tableID, policyName, prettyFlags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_policy_ddl_worker(Oid tableID, Name policyName, int prettyFlags)
+{
+	bool		attrIsNull;
+	Datum		valueDatum;
+	HeapTuple	tuplePolicy;
+	Relation	pgPolicyRel;
+	char	   *targetTable;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	Form_pg_policy policyForm;
+	StringInfoData buf;
+
+	/* Validate that the relation exists */
+	if (!OidIsValid(tableID) || get_rel_name(tableID) == NULL)
+		return NULL;
+
+	initStringInfo(&buf);
+
+	targetTable = generate_qualified_relation_name(tableID);
+	/* Find policy to begin scan */
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(NameStr(*policyName)));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	/* Check that the policy is found, raise an error if not. */
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						NameStr(*policyName),
+						targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	/* Build the CREATE POLICY statement */
+	appendStringInfo(&buf, "CREATE POLICY %s ON %s",
+					 quote_identifier(NameStr(*policyName)),
+					 targetTable);
+
+	/* Check the type is PERMISSIVE or RESTRICTIVE */
+	get_formatted_string(&buf, prettyFlags, 1,
+						 policyForm->polpermissive ? "AS PERMISSIVE" : "AS RESTRICTIVE");
+
+	/* Check command to which the policy applies */
+	get_formatted_string(&buf, prettyFlags, 1, "FOR %s",
+						 get_policy_cmd_name(policyForm->polcmd));
+
+	/* Check if the policy has a TO list */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (i > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, rolename);
+			}
+		}
+
+		if (role_names.len > 0)
+			get_formatted_string(&buf, prettyFlags, 1, "TO %s", role_names.data);
+		else
+
+			/*
+			 * When no specific role is provided, generate the TO clause with
+			 * the PUBLIC role.
+			 */
+			get_formatted_string(&buf, prettyFlags, 1, "TO PUBLIC");
+	}
+
+	/* Check if the policy has a USING expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *usingExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, prettyFlags, 1, "USING (%s)",
+							 text_to_cstring(usingExpression));
+	}
+
+	/* Check if the policy has a WITH CHECK expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *checkExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, prettyFlags, 1, "WITH CHECK (%s)",
+							 text_to_cstring(checkExpression));
+	}
+
+	appendStringInfoChar(&buf, ';');
+
+	/* Clean up. */
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..81053729fba 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,9 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '8811', descr => 'get CREATE statement for policy',
+  proname => 'pg_get_policy_ddl', prorettype => 'text',
+  proargtypes => 'regclass name bool', prosrc => 'pg_get_policy_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h
index f06aa1df439..40e45b738f4 100644
--- a/src/include/commands/policy.h
+++ b/src/include/commands/policy.h
@@ -35,4 +35,6 @@ extern ObjectAddress rename_policy(RenameStmt *stmt);
 
 extern bool relation_has_policies(Relation rel);
 
+extern char *get_policy_cmd_name(char cmd);
+
 #endif							/* POLICY_H */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index c958ef4d70a..4ebe32711cd 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -5101,11 +5101,214 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1');
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+SELECT pg_get_policy_ddl('tab1', NULL);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', NULL);
+                                 ^
+SELECT pg_get_policy_ddl(NULL, NULL);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Test -1 as table oid
+ SELECT pg_get_policy_ddl(-1, 'rls_p1');
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1');
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', 'rls_p1');
+                                 ^
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1');
+ERROR:  policy "pol1" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+                                                                                     pg_get_policy_ddl                                                                                     
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO PUBLIC USING ((dlevel <= (SELECT rls_tbl_2.seclv FROM rls_tbl_2 WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+                                                      pg_get_policy_ddl                                                      
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE FOR ALL TO PUBLIC USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+                                                   pg_get_policy_ddl                                                    
+------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO PUBLIC USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+                                                pg_get_policy_ddl                                                 
+------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR SELECT TO PUBLIC USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+                                                   pg_get_policy_ddl                                                   
+-----------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR INSERT TO PUBLIC WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+                                                pg_get_policy_ddl                                                 
+------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR UPDATE TO PUBLIC USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+                                             pg_get_policy_ddl                                              
+------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR DELETE TO PUBLIC USING ((cid < 8));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+                                                        pg_get_policy_ddl                                                        
+---------------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+                                                                          pg_get_policy_ddl                                                                          
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_exempt_user WITH CHECK ((cid = (SELECT rls_tbl_2.seclv FROM rls_tbl_2)));
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO PUBLIC
+	USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+	AS RESTRICTIVE
+	FOR ALL
+	TO PUBLIC
+	USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO PUBLIC
+	USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR SELECT
+	TO PUBLIC
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR INSERT
+	TO PUBLIC
+	WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR UPDATE
+	TO PUBLIC
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR DELETE
+	TO PUBLIC
+	USING (cid < 8);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_dave, regress_rls_alice
+	USING (true);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_exempt_user
+	WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 30 other objects
+NOTICE:  drop cascades to 32 other objects
 DETAIL:  drop cascades to function f_leak(text)
 drop cascades to table uaccount
 drop cascades to table category
@@ -5136,6 +5339,8 @@ drop cascades to table dep1
 drop cascades to table dep2
 drop cascades to table dob_t1
 drop cascades to table dob_t2
+drop cascades to table rls_tbl_1
+drop cascades to table rls_tbl_2
 DROP USER regress_rls_alice;
 DROP USER regress_rls_bob;
 DROP USER regress_rls_carol;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 5d923c5ca3b..b90f5309578 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2542,6 +2542,86 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1');
+SELECT pg_get_policy_ddl('tab1', NULL);
+SELECT pg_get_policy_ddl(NULL, NULL);
+
+-- Test -1 as table oid
+ SELECT pg_get_policy_ddl(-1, 'rls_p1');
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1');
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1');
+
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-11-07 13:14  Marcos Pegoraro <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Marcos Pegoraro @ 2025-11-07 13:14 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Mark Wong <[email protected]>; Álvaro Herrera <[email protected]>; pgsql-hackers

Em sex., 7 de nov. de 2025 às 09:21, Akshay Joshi <
[email protected]> escreveu:

> Attached is the v7 patch, which is ready for review.
>
>> <https://enterprisedb.com;
>>>
>>> For this functionality to be complete, the
ALTER TABLE ... ENABLE ROW LEVEL SECURITY
statement needs to be added to the command.

Did you think about that ?
Maybe one more parameter ?

regards
Marcos


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-11-07 14:27  Akshay Joshi <[email protected]>
  parent: Marcos Pegoraro <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2025-11-07 14:27 UTC (permalink / raw)
  To: Marcos Pegoraro <[email protected]>; +Cc: Mark Wong <[email protected]>; Álvaro Herrera <[email protected]>; pgsql-hackers

On Fri, Nov 7, 2025 at 6:45 PM Marcos Pegoraro <[email protected]> wrote:

> Em sex., 7 de nov. de 2025 às 09:21, Akshay Joshi <
> [email protected]> escreveu:
>
>> Attached is the v7 patch, which is ready for review.
>>
>>> <https://enterprisedb.com;
>>>>
>>>> For this functionality to be complete, the
> ALTER TABLE ... ENABLE ROW LEVEL SECURITY
> statement needs to be added to the command.
>
> Did you think about that ?
> Maybe one more parameter ?
>

I don’t think we need that statement. Could you please elaborate on where
exactly it needs to be added?
The purpose of this reconstruction DDL is to generate the DDL for an
already created policy. The command you mentioned is used to enable
row-level security on the table, which is a separate step.

>
> regards
> Marcos
>
>
>


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-11-07 14:47  Marcos Pegoraro <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Marcos Pegoraro @ 2025-11-07 14:47 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Mark Wong <[email protected]>; Álvaro Herrera <[email protected]>; pgsql-hackers

Em sex., 7 de nov. de 2025 às 11:27, Akshay Joshi <
[email protected]> escreveu:

> I don’t think we need that statement. Could you please elaborate on where
> exactly it needs to be added?
>

well, these pg_get_..._ddl() functions will be cool for compare/clone
schemas in a multi tenant world.
Instead of dump/sed/restore a schema to create a new one, I could use
something like
select pg_get_table_ddl(oid) from pg_class where nspname = 'customer_050'
and relkind = 'r' union all
select pg_get_constraint_ddl(oid) from pg_constraint inner join pg_class on
... where ... union all
select pg_get_trigger_ddl(oid) from pg_trigger inner join pg_class on
... where ... union all
...

And pg_get_policy_ddl() will be part of these union all selects

Because that would be good to worry about create that only if it does not
exists or drop first too.

regards
Marcos


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-11-10 05:16  Akshay Joshi <[email protected]>
  parent: Marcos Pegoraro <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2025-11-10 05:16 UTC (permalink / raw)
  To: Marcos Pegoraro <[email protected]>; +Cc: Mark Wong <[email protected]>; Álvaro Herrera <[email protected]>; pgsql-hackers

Thanks for the clarification. However, I still believe this is out of scope
for the CREATE POLICY DDL. The command ALTER TABLE ... ENABLE ROW LEVEL
SECURITY seems more appropriate as part of the CREATE TABLE reconstruction
rather than CREATE POLICY.

That said, I’m open to adding it if the majority feels it should be
included in this feature.

On Fri, Nov 7, 2025 at 8:18 PM Marcos Pegoraro <[email protected]> wrote:

> Em sex., 7 de nov. de 2025 às 11:27, Akshay Joshi <
> [email protected]> escreveu:
>
>> I don’t think we need that statement. Could you please elaborate on where
>> exactly it needs to be added?
>>
>
> well, these pg_get_..._ddl() functions will be cool for compare/clone
> schemas in a multi tenant world.
> Instead of dump/sed/restore a schema to create a new one, I could use
> something like
> select pg_get_table_ddl(oid) from pg_class where nspname = 'customer_050'
> and relkind = 'r' union all
> select pg_get_constraint_ddl(oid) from pg_constraint inner join pg_class
> on ... where ... union all
> select pg_get_trigger_ddl(oid) from pg_trigger inner join pg_class on
> ... where ... union all
> ...
>
> And pg_get_policy_ddl() will be part of these union all selects
>
> Because that would be good to worry about create that only if it does not
> exists or drop first too.
>
> regards
> Marcos
>


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2025-11-20 09:27  Akshay Joshi <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2025-11-20 09:27 UTC (permalink / raw)
  To: Marcos Pegoraro <[email protected]>; +Cc: Mark Wong <[email protected]>; Álvaro Herrera <[email protected]>; pgsql-hackers

Attached is the v8 patch for your review, with updated variable names and a
rebase applied.

On Mon, Nov 10, 2025 at 10:46 AM Akshay Joshi <[email protected]>
wrote:

> Thanks for the clarification. However, I still believe this is out of
> scope for the CREATE POLICY DDL. The command ALTER TABLE ... ENABLE ROW
> LEVEL SECURITY seems more appropriate as part of the CREATE TABLE
> reconstruction rather than CREATE POLICY.
>
> That said, I’m open to adding it if the majority feels it should be
> included in this feature.
>
> On Fri, Nov 7, 2025 at 8:18 PM Marcos Pegoraro <[email protected]> wrote:
>
>> Em sex., 7 de nov. de 2025 às 11:27, Akshay Joshi <
>> [email protected]> escreveu:
>>
>>> I don’t think we need that statement. Could you please elaborate on
>>> where exactly it needs to be added?
>>>
>>
>> well, these pg_get_..._ddl() functions will be cool for compare/clone
>> schemas in a multi tenant world.
>> Instead of dump/sed/restore a schema to create a new one, I could use
>> something like
>> select pg_get_table_ddl(oid) from pg_class where nspname = 'customer_050'
>> and relkind = 'r' union all
>> select pg_get_constraint_ddl(oid) from pg_constraint inner join pg_class
>> on ... where ... union all
>> select pg_get_trigger_ddl(oid) from pg_trigger inner join pg_class on
>> ... where ... union all
>> ...
>>
>> And pg_get_policy_ddl() will be part of these union all selects
>>
>> Because that would be good to worry about create that only if it does not
>> exists or drop first too.
>>
>> regards
>> Marcos
>>
>


Attachments:

  [application/x-patch] v8-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch (26.1K, ../../CANxoLDfD_MnK+2=XrWr_fZRh1qbLriO=7MbbZ9UAcwktydFnBA@mail.gmail.com/3-v8-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From 2421b3821457ddc765da70667fde2aed437b30bf Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 10 Oct 2025 15:46:13 +0530
Subject: [PATCH v8] Add pg_get_policy_ddl() function to reconstruct CREATE
 POLICY statements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduces a new system function:
pg_get_policy_ddl(regclass table, name policy_name, bool pretty),
which reconstructs the CREATE POLICY statement for the specified policy.

Usage examples:
SELECT pg_get_policy_ddl('rls_table', 'pol1'); -- non-pretty formatted DDL
SELECT pg_get_policy_ddl('rls_table', 'pol1', true);  -- pretty formatted DDL
SELECT pg_get_policy_ddl(16564, 'pol1'); -- non-pretty formatted DDL
SELECT pg_get_policy_ddl(16564, 'pol1', true);  -- pretty formatted DDL

Reference: PG-163
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  55 ++++++
 src/backend/catalog/system_functions.sql  |   6 +
 src/backend/commands/policy.c             |  27 +++
 src/backend/utils/adt/ruleutils.c         | 212 ++++++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   3 +
 src/include/commands/policy.h             |   2 +
 src/test/regress/expected/rowsecurity.out | 207 ++++++++++++++++++++-
 src/test/regress/sql/rowsecurity.sql      |  80 ++++++++
 8 files changed, 591 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..edfd9381be0 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,59 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </para>
+
+   <table id="functions-get-object-ddl-table">
+    <title>Get Object DDL Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>, <parameter>policy_name</parameter> <type>name</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement from the
+        system catalogs for a specified table and policy name. The result is a
+        comprehensive <command>CREATE POLICY</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+   Most of the functions that reconstruct (decompile) database objects have an
+   optional <parameter>pretty</parameter> flag, which if
+   <literal>true</literal> causes the result to be
+   <quote>pretty-printed</quote>. Pretty-printing adds tab character and new
+   line character for legibility. Passing <literal>false</literal> for the
+   <parameter>pretty</parameter> parameter yields the same result as omitting
+   the parameter.
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..a5e22374668 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,12 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+  pg_get_policy_ddl(tableID regclass, policyName name, pretty bool DEFAULT false)
+RETURNS text
+LANGUAGE INTERNAL
+AS 'pg_get_policy_ddl';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c
index 83056960fe4..1abe0c44353 100644
--- a/src/backend/commands/policy.c
+++ b/src/backend/commands/policy.c
@@ -128,6 +128,33 @@ parse_policy_command(const char *cmd_name)
 	return polcmd;
 }
 
+/*
+ * get_policy_cmd_name -
+ *	 helper function to convert char representation to full command strings.
+ *
+ * cmd -  Valid values are '*', 'r', 'a', 'w' and 'd'.
+ *
+ */
+char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command");
+	}
+}
+
 /*
  * policy_role_list_to_array
  *	 helper function to convert a list of RoleSpecs to an array of
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..9adadf65743 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -33,12 +33,14 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
+#include "commands/policy.h"
 #include "common/keywords.h"
 #include "executor/spi.h"
 #include "funcapi.h"
@@ -94,6 +96,10 @@
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#define GET_DDL_PRETTY_FLAGS(pretty) \
+	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
+	 : 0)
+
 /* Default line length for pretty-print wrapping: 0 means wrap always */
 #define WRAP_COLUMN_DEFAULT		0
 
@@ -546,6 +552,12 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
 										  deparse_context *context,
 										  bool showimplicit,
 										  bool needcomma);
+static void get_formatted_string(StringInfo buf,
+								 int prettyFlags,
+								 int nTabChars,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_policy_ddl_worker(Oid tableID, Name policyName,
+									  int prettyFlags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13743,3 +13755,203 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags the output includes tabs (\t) and
+ *               newlines (\n).
+ * nTabChars - indent with specified no of tabs.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int nTabChars, const char *fmt,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < nTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+/*
+ * pg_get_policy_ddl
+ *
+ * Generate a CREATE POLICY statement for the specified policy.
+ *
+ * tableID - Table ID of the policy.
+ * policyName - Name of the policy for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	bool		pretty = PG_GETARG_BOOL(2);
+	int			prettyFlags;
+	char	   *res;
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+	res = pg_get_policy_ddl_worker(tableID, policyName, prettyFlags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_policy_ddl_worker(Oid tableID, Name policyName, int prettyFlags)
+{
+	bool		attrIsNull;
+	Datum		valueDatum;
+	HeapTuple	tuplePolicy;
+	Relation	pgPolicyRel;
+	char	   *targetTable;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	Form_pg_policy policyForm;
+	StringInfoData buf;
+
+	/* Validate that the relation exists */
+	if (!OidIsValid(tableID) || get_rel_name(tableID) == NULL)
+		return NULL;
+
+	initStringInfo(&buf);
+
+	targetTable = generate_qualified_relation_name(tableID);
+	/* Find policy to begin scan */
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(NameStr(*policyName)));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	/* Check that the policy is found, raise an error if not. */
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						NameStr(*policyName),
+						targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	/* Build the CREATE POLICY statement */
+	appendStringInfo(&buf, "CREATE POLICY %s ON %s",
+					 quote_identifier(NameStr(*policyName)),
+					 targetTable);
+
+	/* Check the type is PERMISSIVE or RESTRICTIVE */
+	get_formatted_string(&buf, prettyFlags, 1,
+						 policyForm->polpermissive ? "AS PERMISSIVE" : "AS RESTRICTIVE");
+
+	/* Check command to which the policy applies */
+	get_formatted_string(&buf, prettyFlags, 1, "FOR %s",
+						 get_policy_cmd_name(policyForm->polcmd));
+
+	/* Check if the policy has a TO list */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (i > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, rolename);
+			}
+		}
+
+		if (role_names.len > 0)
+			get_formatted_string(&buf, prettyFlags, 1, "TO %s", role_names.data);
+		else
+
+			/*
+			 * When no specific role is provided, generate the TO clause with
+			 * the PUBLIC role.
+			 */
+			get_formatted_string(&buf, prettyFlags, 1, "TO PUBLIC");
+	}
+
+	/* Check if the policy has a USING expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *usingExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, prettyFlags, 1, "USING (%s)",
+							 text_to_cstring(usingExpression));
+	}
+
+	/* Check if the policy has a WITH CHECK expr */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		text	   *exprtext = DatumGetTextPP(valueDatum);
+		text	   *checkExpression = pg_get_expr_worker(exprtext,
+														 policyForm->polrelid,
+														 prettyFlags);
+
+		get_formatted_string(&buf, prettyFlags, 1, "WITH CHECK (%s)",
+							 text_to_cstring(checkExpression));
+	}
+
+	appendStringInfoChar(&buf, ';');
+
+	/* Clean up. */
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index aaadfd8c748..50fb26f6b1e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,9 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '8811', descr => 'get CREATE statement for policy',
+  proname => 'pg_get_policy_ddl', prorettype => 'text',
+  proargtypes => 'regclass name bool', prosrc => 'pg_get_policy_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h
index f06aa1df439..40e45b738f4 100644
--- a/src/include/commands/policy.h
+++ b/src/include/commands/policy.h
@@ -35,4 +35,6 @@ extern ObjectAddress rename_policy(RenameStmt *stmt);
 
 extern bool relation_has_policies(Relation rel);
 
+extern char *get_policy_cmd_name(char cmd);
+
 #endif							/* POLICY_H */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index c958ef4d70a..4ebe32711cd 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -5101,11 +5101,214 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1');
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+SELECT pg_get_policy_ddl('tab1', NULL);
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', NULL);
+                                 ^
+SELECT pg_get_policy_ddl(NULL, NULL);
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Test -1 as table oid
+ SELECT pg_get_policy_ddl(-1, 'rls_p1');
+ pg_get_policy_ddl 
+-------------------
+ 
+(1 row)
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1');
+ERROR:  relation "tab1" does not exist
+LINE 1: SELECT pg_get_policy_ddl('tab1', 'rls_p1');
+                                 ^
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1');
+ERROR:  policy "pol1" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+                                                                                     pg_get_policy_ddl                                                                                     
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO PUBLIC USING ((dlevel <= (SELECT rls_tbl_2.seclv FROM rls_tbl_2 WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+                                                      pg_get_policy_ddl                                                      
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE FOR ALL TO PUBLIC USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+                                                   pg_get_policy_ddl                                                    
+------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO PUBLIC USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+                                                pg_get_policy_ddl                                                 
+------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR SELECT TO PUBLIC USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+                                                   pg_get_policy_ddl                                                   
+-----------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR INSERT TO PUBLIC WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+                                                pg_get_policy_ddl                                                 
+------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR UPDATE TO PUBLIC USING (((cid % 2) = 0));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+                                             pg_get_policy_ddl                                              
+------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR DELETE TO PUBLIC USING ((cid < 8));
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+                                                        pg_get_policy_ddl                                                        
+---------------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+                                                                          pg_get_policy_ddl                                                                          
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 AS PERMISSIVE FOR ALL TO regress_rls_exempt_user WITH CHECK ((cid = (SELECT rls_tbl_2.seclv FROM rls_tbl_2)));
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO PUBLIC
+	USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+	AS RESTRICTIVE
+	FOR ALL
+	TO PUBLIC
+	USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO PUBLIC
+	USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR SELECT
+	TO PUBLIC
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR INSERT
+	TO PUBLIC
+	WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR UPDATE
+	TO PUBLIC
+	USING ((cid % 2) = 0);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR DELETE
+	TO PUBLIC
+	USING (cid < 8);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_dave, regress_rls_alice
+	USING (true);
+(1 row)
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+	AS PERMISSIVE
+	FOR ALL
+	TO regress_rls_exempt_user
+	WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 30 other objects
+NOTICE:  drop cascades to 32 other objects
 DETAIL:  drop cascades to function f_leak(text)
 drop cascades to table uaccount
 drop cascades to table category
@@ -5136,6 +5339,8 @@ drop cascades to table dep1
 drop cascades to table dep2
 drop cascades to table dob_t1
 drop cascades to table dob_t2
+drop cascades to table rls_tbl_1
+drop cascades to table rls_tbl_2
 DROP USER regress_rls_alice;
 DROP USER regress_rls_bob;
 DROP USER regress_rls_carol;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 5d923c5ca3b..b90f5309578 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2542,6 +2542,86 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(tableName, policyName, pretty) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- Test NULL value
+SELECT pg_get_policy_ddl(NULL, 'rls_p1');
+SELECT pg_get_policy_ddl('tab1', NULL);
+SELECT pg_get_policy_ddl(NULL, NULL);
+
+-- Test -1 as table oid
+ SELECT pg_get_policy_ddl(-1, 'rls_p1');
+
+-- Table does not exist
+SELECT pg_get_policy_ddl('tab1', 'rls_p1');
+-- Policy does not exist
+SELECT pg_get_policy_ddl('rls_tbl_1', 'pol1');
+
+-- Without Pretty formatted
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', false);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', false);
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+SELECT pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-01-05 14:30  jian he <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: jian he @ 2026-01-05 14:30 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Marcos Pegoraro <[email protected]>; Mark Wong <[email protected]>; Álvaro Herrera <[email protected]>; pgsql-hackers

On Thu, Nov 20, 2025 at 5:27 PM Akshay Joshi
<[email protected]> wrote:
>
> Attached is the v8 patch for your review, with updated variable names and a rebase applied.
>
hi.

+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>,
<parameter>policy_name</parameter> <type>name</type>, <optional>
<parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement from the
+        system catalogs for a specified table and policy name. The result is a
+        comprehensive <command>CREATE POLICY</command> statement.
+       </para></entry>
+      </row>
+     </tbody>

 ( <parameter>table</parameter> <type>regclass</type> ...
this line is way too long, we can split it into several lines, it
won't affect the appearance.

like:
        <function>pg_get_policy_ddl</function>
        ( <parameter>table</parameter> <type>regclass</type>,
          <parameter>policy_name</parameter> <type>name</type>,
          <optional> <parameter>pretty</parameter>
<type>boolean</type> </optional> )
        <returnvalue>text</returnvalue>

Also, the explanation does not mention that the default value of
pretty is false.


index 2d946d6d9e9..a5e22374668 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,12 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';

+CREATE OR REPLACE FUNCTION
+  pg_get_policy_ddl(tableID regclass, policyName name, pretty bool
DEFAULT false)
+RETURNS text
+LANGUAGE INTERNAL
+AS 'pg_get_policy_ddl';
+

The partial upper casing above has no effect; it's the same as
``pg_get_policy_ddl(tableid regclass, policyname name, pretty bool
DEFAULT false)``

--
jian
https://www.enterprisedb.com/






^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-05-22 13:32  Akshay Joshi <[email protected]>
  parent: jian he <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2026-05-22 13:32 UTC (permalink / raw)
  To: jian he <[email protected]>; +Cc: Marcos Pegoraro <[email protected]>; Mark Wong <[email protected]>; Álvaro Herrera <[email protected]>; pgsql-hackers

Hi hackers,


Following the recently committed *pg_get_database_ddl()*, which adopted a
VARIADIC options text[] style for DDL-reconstruction functions, here is a
patch in the same spirit for row-level security policies.

The new function:
    pg_get_policy_ddl(table regclass, policy_name name, VARIADIC options
text[]) RETURNS setof text

Reconstructs the CREATE POLICY statement for the named policy on the given
table, returning the result as a single row.

The currently supported option is pretty (boolean) for formatted output.

    SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1');
    SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1', 'pretty', 'true');

NULL inputs for table or policy_name return no rows. Unknown option names,
invalid boolean values, and duplicate options are reported as errors
consistent with the pattern established by pg_get_database_ddl().

The patch includes documentation updates in func-info.sgml and regression
tests in rowsecurity.sql covering PERMISSIVE/RESTRICTIVE, each command type
(ALL/SELECT/INSERT/UPDATE/DELETE), TO role lists, both USING and WITH CHECK
clauses, pretty/non-pretty output, and the error paths above.

Patch is ready for review.


On Mon, Jan 5, 2026 at 8:00 PM jian he <[email protected]> wrote:

> On Thu, Nov 20, 2025 at 5:27 PM Akshay Joshi
> <[email protected]> wrote:
> >
> > Attached is the v8 patch for your review, with updated variable names
> and a rebase applied.
> >
> hi.
>
> +     <tbody>
> +      <row>
> +       <entry role="func_table_entry"><para role="func_signature">
> +        <indexterm>
> +         <primary>pg_get_policy_ddl</primary>
> +        </indexterm>
> +        <function>pg_get_policy_ddl</function>
> +        ( <parameter>table</parameter> <type>regclass</type>,
> <parameter>policy_name</parameter> <type>name</type>, <optional>
> <parameter>pretty</parameter> <type>boolean</type> </optional> )
> +        <returnvalue>text</returnvalue>
> +       </para>
> +       <para>
> +        Reconstructs the <command>CREATE POLICY</command> statement from
> the
> +        system catalogs for a specified table and policy name. The result
> is a
> +        comprehensive <command>CREATE POLICY</command> statement.
> +       </para></entry>
> +      </row>
> +     </tbody>
>
>  ( <parameter>table</parameter> <type>regclass</type> ...
> this line is way too long, we can split it into several lines, it
> won't affect the appearance.
>
> like:
>         <function>pg_get_policy_ddl</function>
>         ( <parameter>table</parameter> <type>regclass</type>,
>           <parameter>policy_name</parameter> <type>name</type>,
>           <optional> <parameter>pretty</parameter>
> <type>boolean</type> </optional> )
>         <returnvalue>text</returnvalue>
>
> Also, the explanation does not mention that the default value of
> pretty is false.
>
>
> index 2d946d6d9e9..a5e22374668 100644
> --- a/src/backend/catalog/system_functions.sql
> +++ b/src/backend/catalog/system_functions.sql
> @@ -657,6 +657,12 @@ LANGUAGE INTERNAL
>  STRICT VOLATILE PARALLEL UNSAFE
>  AS 'pg_replication_origin_session_setup';
>
> +CREATE OR REPLACE FUNCTION
> +  pg_get_policy_ddl(tableID regclass, policyName name, pretty bool
> DEFAULT false)
> +RETURNS text
> +LANGUAGE INTERNAL
> +AS 'pg_get_policy_ddl';
> +
>
> The partial upper casing above has no effect; it's the same as
> ``pg_get_policy_ddl(tableid regclass, policyname name, pretty bool
> DEFAULT false)``
>
> --
> jian
> https://www.enterprisedb.com/
>


Attachments:

  [application/octet-stream] v9-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch (24.0K, ../../CANxoLDfdZTLLJqXnnfUYG-Uw4LHBKKnB5f1XOdaQ3ZET=K1qnw@mail.gmail.com/3-v9-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From aae0e58182b1dce11e771a0f14dcc1ab142f647e Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 22 May 2026 18:18:07 +0530
Subject: [PATCH v9] Add pg_get_policy_ddl() function to reconstruct CREATE
 POLICY statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduces a new system function:
    pg_get_policy_ddl(table regclass, policy_name name,
       VARIADIC options text[]) RETURNS setof text

which reconstructs the CREATE POLICY statement for the named row-level
security policy on the specified table. The result is returned as a single row.
 
The supported option is:

    pretty (boolean) - format the output for readability.

Usage examples:
    -- non-pretty formatted DDL (default)
    SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1');
    SELECT * FROM pg_get_policy_ddl(16564, 'pol1');

    -- pretty formatted DDL
    SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1', 'pretty', 'true');
    SELECT * FROM pg_get_policy_ddl(16564, 'pol1', 'pretty', 'true');

Reference: PG-163
Author: Akshay Joshi <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  20 ++
 src/backend/utils/adt/ddlutils.c          | 262 ++++++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   8 +
 src/test/regress/expected/rowsecurity.out | 193 ++++++++++++++++
 src/test/regress/sql/rowsecurity.sql      |  87 +++++++
 5 files changed, 570 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 00f64f50ceb..44bf6455bb1 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3961,6 +3961,26 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
         <literal>TABLESPACE</literal>.
        </para></entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>,
+        <parameter>policy_name</parameter> <type>name</type>
+        <optional>, <literal>VARIADIC</literal> <parameter>options</parameter>
+        <type>text</type> </optional> )
+        <returnvalue>setof text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement for the
+        named row-level security policy on the specified table.  The result
+        is returned as a single row.
+        The following option is supported: <literal>pretty</literal>
+        (boolean) for formatted output.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c
index f32fcd453ef..728d3648979 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_tablespace.h"
 #include "commands/tablespace.h"
 #include "common/relpath.h"
@@ -86,6 +87,9 @@ static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner
 static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull);
 static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
 										  bool no_owner, bool no_tablespace);
+static List *pg_get_policy_ddl_internal(Oid tableID, const char *policyName,
+										bool pretty);
+static const char *get_policy_cmd_name(char cmd);
 
 
 /*
@@ -1185,3 +1189,261 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
 		SRF_RETURN_DONE(funcctx);
 	}
 }
+
+/*
+ * get_policy_cmd_name
+ *		Map a pg_policy.polcmd char to its SQL keyword.
+ */
+static const char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command: %d", (int) cmd);
+	}
+}
+
+/*
+ * pg_get_policy_ddl_internal
+ *		Generate the DDL statement to recreate a row-level security policy.
+ *
+ * Returns a List containing a single palloc'd string with the CREATE POLICY
+ * statement.  Returning a List keeps the calling convention consistent with
+ * the rest of the pg_get_*_ddl family even though only one row is produced.
+ */
+static List *
+pg_get_policy_ddl_internal(Oid tableID, const char *policyName, bool pretty)
+{
+	Relation	pgPolicyRel;
+	HeapTuple	tuplePolicy;
+	Form_pg_policy policyForm;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	StringInfoData buf;
+	Datum		valueDatum;
+	bool		attrIsNull;
+	char	   *targetTable;
+	List	   *statements = NIL;
+
+	/* Validate that the relation exists */
+	{
+		char	   *relname = get_rel_name(tableID);
+		char	   *nspname;
+
+		if (relname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_TABLE),
+					 errmsg("relation with OID %u does not exist", tableID)));
+
+		nspname = get_namespace_name(get_rel_namespace(tableID));
+		if (nspname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_SCHEMA),
+					 errmsg("schema for relation with OID %u does not exist",
+							tableID)));
+
+		targetTable = quote_qualified_identifier(nspname, relname);
+		pfree(relname);
+		pfree(nspname);
+	}
+
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(policyName));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						policyName, targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	initStringInfo(&buf);
+
+	/* Build the CREATE POLICY statement */
+	appendStringInfo(&buf, "CREATE POLICY %s ON %s",
+					 quote_identifier(policyName),
+					 targetTable);
+
+	/*
+	 * Emit AS RESTRICTIVE only when it differs from the default (PERMISSIVE).
+	 */
+	if (!policyForm->polpermissive)
+		append_ddl_option(&buf, pretty, 4, "AS RESTRICTIVE");
+
+	/*
+	 * Emit FOR <cmd> only when it differs from the default (ALL, encoded as
+	 * '*').
+	 */
+	if (policyForm->polcmd != '*')
+		append_ddl_option(&buf, pretty, 4, "FOR %s",
+						  get_policy_cmd_name(policyForm->polcmd));
+
+	/*
+	 * Emit TO <roles> only when it differs from the default (PUBLIC).  PUBLIC
+	 * is encoded in polroles as a single InvalidOid element, so we omit the
+	 * clause whenever every entry is InvalidOid.
+	 */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (role_names.len > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, quote_identifier(rolename));
+			}
+		}
+
+		if (role_names.len > 0)
+			append_ddl_option(&buf, pretty, 4, "TO %s", role_names.data);
+
+		pfree(role_names.data);
+		pfree(policy_roles);
+	}
+
+	/* USING expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "USING (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	/* WITH CHECK expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "WITH CHECK (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	appendStringInfoChar(&buf, ';');
+
+	statements = lappend(statements, pstrdup(buf.data));
+
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+	pfree(buf.data);
+
+	return statements;
+}
+
+/*
+ * pg_get_policy_ddl
+ *		Return DDL to recreate a row-level security policy as a single text row.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	List	   *statements;
+
+	if (SRF_IS_FIRSTCALL())
+	{
+		MemoryContext oldcontext;
+		Oid			tableID;
+		Name		policyName;
+		DdlOption	opts[] = {
+			{"pretty", DDL_OPT_BOOL},
+		};
+
+		funcctx = SRF_FIRSTCALL_INIT();
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+		{
+			MemoryContextSwitchTo(oldcontext);
+			SRF_RETURN_DONE(funcctx);
+		}
+
+		tableID = PG_GETARG_OID(0);
+		policyName = PG_GETARG_NAME(1);
+
+		parse_ddl_options(fcinfo, 2, opts, lengthof(opts));
+
+		statements = pg_get_policy_ddl_internal(tableID,
+												NameStr(*policyName),
+												opts[0].isset && opts[0].boolval);
+		funcctx->user_fctx = statements;
+		funcctx->max_calls = list_length(statements);
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	funcctx = SRF_PERCALL_SETUP();
+	statements = (List *) funcctx->user_fctx;
+
+	if (funcctx->call_cntr < funcctx->max_calls)
+	{
+		char	   *stmt;
+
+		stmt = list_nth(statements, funcctx->call_cntr);
+
+		SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+	}
+	else
+	{
+		list_free_deep(statements);
+		SRF_RETURN_DONE(funcctx);
+	}
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..3e32c89fd6d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,14 @@
   proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
   proargmodes => '{i,v}', proargdefaults => '{NULL}',
   prosrc => 'pg_get_database_ddl' },
+{ oid => '6517', descr => 'get DDL to recreate a row-level security policy',
+  proname => 'pg_get_policy_ddl', prorows => '1', provariadic => 'text',
+  proisstrict => 'f', proretset => 't', provolatile => 's',
+  pronargdefaults => '1', prorettype => 'text',
+  proargtypes => 'regclass name text',
+  proallargtypes => '{regclass,name,text}',
+  proargmodes => '{i,i,v}', proargdefaults => '{NULL}',
+  prosrc => 'pg_get_policy_ddl' },
 { oid => '2509',
   descr => 'deparse an encoded expression with pretty-print option',
   proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 3a5e82c35bd..9a20536584d 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -5195,6 +5195,199 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(table, policy_name, VARIADIC options) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+ count 
+-------
+     0
+(1 row)
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+ERROR:  relation "nonexistent_tbl" does not exist
+LINE 1: SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1')...
+                                        ^
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+ERROR:  policy "nonexistent_pol" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Invalid option name
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'badopt', 'true');
+ERROR:  unrecognized option: "badopt"
+-- Invalid boolean value for option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'maybe');
+ERROR:  invalid value for boolean option "pretty": maybe
+-- Duplicate option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true', 'pretty', 'false');
+ERROR:  option "pretty" is specified more than once
+-- Without pretty formatting (default)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                              +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+                                   pg_get_policy_ddl                                    
+----------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 FOR SELECT USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+                                       pg_get_policy_ddl                                       
+-----------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 FOR INSERT WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'false');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'false');
+                                 pg_get_policy_ddl                                  
+------------------------------------------------------------------------------------
+ CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 FOR DELETE USING ((cid < 8));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'false');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'false');
+                                                      pg_get_policy_ddl                                                      
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 TO regress_rls_exempt_user WITH CHECK ((cid = ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2)));
+(1 row)
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+    AS RESTRICTIVE
+    USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+    FOR SELECT
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+    FOR INSERT
+    WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+    FOR UPDATE
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+    FOR DELETE
+    USING (cid < 8);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_dave, regress_rls_alice
+    USING (true);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_exempt_user
+    WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+\pset format aligned
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 6b3566271df..aca6102de71 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2598,6 +2598,93 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(table, policy_name, VARIADIC options) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+
+-- Invalid option name
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'badopt', 'true');
+-- Invalid boolean value for option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'maybe');
+-- Duplicate option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true', 'pretty', 'false');
+
+-- Without pretty formatting (default)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'false');
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'true');
+\pset format aligned
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-05-22 16:24  Japin Li <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Japin Li @ 2026-05-22 16:24 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: jian he <[email protected]>; Marcos Pegoraro <[email protected]>; Mark Wong <[email protected]>; Álvaro Herrera <[email protected]>; pgsql-hackers


Hi, Akshay

On Fri, 22 May 2026 at 19:02, Akshay Joshi <[email protected]> wrote:
> Hi hackers,                                                                                                              
>                      
>
> Following the recently committed pg_get_database_ddl(), which adopted a VARIADIC options text[] style for
> DDL-reconstruction functions, here is a patch in the same spirit for row-level security policies.
>
> The new function:
>     pg_get_policy_ddl(table regclass, policy_name name, VARIADIC options text[]) RETURNS setof text 
>
> Reconstructs the CREATE POLICY statement for the named policy on the given table, returning the result as a single row.
>
> The currently supported option is pretty (boolean) for formatted output.                                                 
>       
>     SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1');
>     SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1', 'pretty', 'true');
>
> NULL inputs for table or policy_name return no rows. Unknown option names, invalid boolean values, and duplicate options
> are reported as errors consistent with the pattern established by pg_get_database_ddl(). 
>
> The patch includes documentation updates in func-info.sgml and regression tests in rowsecurity.sql covering
> PERMISSIVE/RESTRICTIVE, each command type (ALL/SELECT/INSERT/UPDATE/DELETE), TO role lists, both USING and WITH CHECK
> clauses, pretty/non-pretty output, and the error paths above.
>
> Patch is ready for review.
>
> On Mon, Jan 5, 2026 at 8:00 PM jian he <[email protected]> wrote:
>
>  On Thu, Nov 20, 2025 at 5:27 PM Akshay Joshi
>  <[email protected]> wrote:
>  >
>  > Attached is the v8 patch for your review, with updated variable names and a rebase applied.
>  >
>  hi.
>
>  +     <tbody>
>  +      <row>
>  +       <entry role="func_table_entry"><para role="func_signature">
>  +        <indexterm>
>  +         <primary>pg_get_policy_ddl</primary>
>  +        </indexterm>
>  +        <function>pg_get_policy_ddl</function>
>  +        ( <parameter>table</parameter> <type>regclass</type>,
>  <parameter>policy_name</parameter> <type>name</type>, <optional>
>  <parameter>pretty</parameter> <type>boolean</type> </optional> )
>  +        <returnvalue>text</returnvalue>
>  +       </para>
>  +       <para>
>  +        Reconstructs the <command>CREATE POLICY</command> statement from the
>  +        system catalogs for a specified table and policy name. The result is a
>  +        comprehensive <command>CREATE POLICY</command> statement.
>  +       </para></entry>
>  +      </row>
>  +     </tbody>
>
>   ( <parameter>table</parameter> <type>regclass</type> ...
>  this line is way too long, we can split it into several lines, it
>  won't affect the appearance.
>
>  like:
>          <function>pg_get_policy_ddl</function>
>          ( <parameter>table</parameter> <type>regclass</type>,
>            <parameter>policy_name</parameter> <type>name</type>,
>            <optional> <parameter>pretty</parameter>
>  <type>boolean</type> </optional> )
>          <returnvalue>text</returnvalue>
>
>  Also, the explanation does not mention that the default value of
>  pretty is false.
>
>  index 2d946d6d9e9..a5e22374668 100644
>  --- a/src/backend/catalog/system_functions.sql
>  +++ b/src/backend/catalog/system_functions.sql
>  @@ -657,6 +657,12 @@ LANGUAGE INTERNAL
>   STRICT VOLATILE PARALLEL UNSAFE
>   AS 'pg_replication_origin_session_setup';
>
>  +CREATE OR REPLACE FUNCTION
>  +  pg_get_policy_ddl(tableID regclass, policyName name, pretty bool
>  DEFAULT false)
>  +RETURNS text
>  +LANGUAGE INTERNAL
>  +AS 'pg_get_policy_ddl';
>  +
>
>  The partial upper casing above has no effect; it's the same as
>  ``pg_get_policy_ddl(tableid regclass, policyname name, pretty bool
>  DEFAULT false)``
>

Thanks for updating the patch.  Just one nitpick below.

+		append_ddl_option(&buf, pretty, 4, "USING (%s)",
+						  TextDatumGetCString(expr));

The expression string already contains the parentheses, so we can omit them
here, as well as in the WITH CHECK clause.

-- 
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.






^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-05-25 07:17  Akshay Joshi <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2026-05-25 07:17 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: jian he <[email protected]>; Marcos Pegoraro <[email protected]>; Mark Wong <[email protected]>; Álvaro Herrera <[email protected]>; pgsql-hackers

Thanks Japin,

Attached is the updated patch.

On Fri, May 22, 2026 at 9:55 PM Japin Li <[email protected]> wrote:

>
> Hi, Akshay
>
> On Fri, 22 May 2026 at 19:02, Akshay Joshi <[email protected]>
> wrote:
> > Hi hackers,
>
> >
> >
> > Following the recently committed pg_get_database_ddl(), which adopted a
> VARIADIC options text[] style for
> > DDL-reconstruction functions, here is a patch in the same spirit for
> row-level security policies.
> >
> > The new function:
> >     pg_get_policy_ddl(table regclass, policy_name name, VARIADIC options
> text[]) RETURNS setof text
> >
> > Reconstructs the CREATE POLICY statement for the named policy on the
> given table, returning the result as a single row.
> >
> > The currently supported option is pretty (boolean) for formatted
> output.
> >
> >     SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1');
> >     SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1', 'pretty',
> 'true');
> >
> > NULL inputs for table or policy_name return no rows. Unknown option
> names, invalid boolean values, and duplicate options
> > are reported as errors consistent with the pattern established by
> pg_get_database_ddl().
> >
> > The patch includes documentation updates in func-info.sgml and
> regression tests in rowsecurity.sql covering
> > PERMISSIVE/RESTRICTIVE, each command type
> (ALL/SELECT/INSERT/UPDATE/DELETE), TO role lists, both USING and WITH CHECK
> > clauses, pretty/non-pretty output, and the error paths above.
> >
> > Patch is ready for review.
> >
> > On Mon, Jan 5, 2026 at 8:00 PM jian he <[email protected]>
> wrote:
> >
> >  On Thu, Nov 20, 2025 at 5:27 PM Akshay Joshi
> >  <[email protected]> wrote:
> >  >
> >  > Attached is the v8 patch for your review, with updated variable names
> and a rebase applied.
> >  >
> >  hi.
> >
> >  +     <tbody>
> >  +      <row>
> >  +       <entry role="func_table_entry"><para role="func_signature">
> >  +        <indexterm>
> >  +         <primary>pg_get_policy_ddl</primary>
> >  +        </indexterm>
> >  +        <function>pg_get_policy_ddl</function>
> >  +        ( <parameter>table</parameter> <type>regclass</type>,
> >  <parameter>policy_name</parameter> <type>name</type>, <optional>
> >  <parameter>pretty</parameter> <type>boolean</type> </optional> )
> >  +        <returnvalue>text</returnvalue>
> >  +       </para>
> >  +       <para>
> >  +        Reconstructs the <command>CREATE POLICY</command> statement
> from the
> >  +        system catalogs for a specified table and policy name. The
> result is a
> >  +        comprehensive <command>CREATE POLICY</command> statement.
> >  +       </para></entry>
> >  +      </row>
> >  +     </tbody>
> >
> >   ( <parameter>table</parameter> <type>regclass</type> ...
> >  this line is way too long, we can split it into several lines, it
> >  won't affect the appearance.
> >
> >  like:
> >          <function>pg_get_policy_ddl</function>
> >          ( <parameter>table</parameter> <type>regclass</type>,
> >            <parameter>policy_name</parameter> <type>name</type>,
> >            <optional> <parameter>pretty</parameter>
> >  <type>boolean</type> </optional> )
> >          <returnvalue>text</returnvalue>
> >
> >  Also, the explanation does not mention that the default value of
> >  pretty is false.
> >
> >  index 2d946d6d9e9..a5e22374668 100644
> >  --- a/src/backend/catalog/system_functions.sql
> >  +++ b/src/backend/catalog/system_functions.sql
> >  @@ -657,6 +657,12 @@ LANGUAGE INTERNAL
> >   STRICT VOLATILE PARALLEL UNSAFE
> >   AS 'pg_replication_origin_session_setup';
> >
> >  +CREATE OR REPLACE FUNCTION
> >  +  pg_get_policy_ddl(tableID regclass, policyName name, pretty bool
> >  DEFAULT false)
> >  +RETURNS text
> >  +LANGUAGE INTERNAL
> >  +AS 'pg_get_policy_ddl';
> >  +
> >
> >  The partial upper casing above has no effect; it's the same as
> >  ``pg_get_policy_ddl(tableid regclass, policyname name, pretty bool
> >  DEFAULT false)``
> >
>
> Thanks for updating the patch.  Just one nitpick below.
>
> +               append_ddl_option(&buf, pretty, 4, "USING (%s)",
> +
>  TextDatumGetCString(expr));
>
> The expression string already contains the parentheses, so we can omit them
> here, as well as in the WITH CHECK clause.
>
> --
> Regards,
> Japin Li
> ChengDu WenWu Information Technology Co., Ltd.
>


Attachments:

  [application/octet-stream] v10-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch (24.3K, ../../CANxoLDffrZGRTGpW_sPQ-hPEYs0hgjaFgJQh3PJFpPu5Zsbgvg@mail.gmail.com/3-v10-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From e3c100793993f009ba5245b9d8a566cf832d2d88 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 22 May 2026 18:18:07 +0530
Subject: [PATCH v10] Add pg_get_policy_ddl() function to reconstruct CREATE
 POLICY statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduces a new system function:
    pg_get_policy_ddl(table regclass, policy_name name,
       VARIADIC options text[]) RETURNS setof text

which reconstructs the CREATE POLICY statement for the named row-level
security policy on the specified table. The result is returned as a single row.
 
The supported option is:

    pretty (boolean) - format the output for readability.

Usage examples:
    -- non-pretty formatted DDL (default)
    SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1');
    SELECT * FROM pg_get_policy_ddl(16564, 'pol1');

    -- pretty formatted DDL
    SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1', 'pretty', 'true');
    SELECT * FROM pg_get_policy_ddl(16564, 'pol1', 'pretty', 'true');

Reference: PG-163
Author: Akshay Joshi <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  20 ++
 src/backend/utils/adt/ddlutils.c          | 270 ++++++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   8 +
 src/test/regress/expected/rowsecurity.out | 193 ++++++++++++++++
 src/test/regress/sql/rowsecurity.sql      |  87 +++++++
 5 files changed, 578 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 00f64f50ceb..44bf6455bb1 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3961,6 +3961,26 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
         <literal>TABLESPACE</literal>.
        </para></entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>,
+        <parameter>policy_name</parameter> <type>name</type>
+        <optional>, <literal>VARIADIC</literal> <parameter>options</parameter>
+        <type>text</type> </optional> )
+        <returnvalue>setof text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement for the
+        named row-level security policy on the specified table.  The result
+        is returned as a single row.
+        The following option is supported: <literal>pretty</literal>
+        (boolean) for formatted output.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c
index f32fcd453ef..15696b8c801 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_tablespace.h"
 #include "commands/tablespace.h"
 #include "common/relpath.h"
@@ -86,6 +87,9 @@ static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner
 static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull);
 static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
 										  bool no_owner, bool no_tablespace);
+static List *pg_get_policy_ddl_internal(Oid tableID, const char *policyName,
+										bool pretty);
+static const char *get_policy_cmd_name(char cmd);
 
 
 /*
@@ -1185,3 +1189,269 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
 		SRF_RETURN_DONE(funcctx);
 	}
 }
+
+/*
+ * get_policy_cmd_name
+ *		Map a pg_policy.polcmd char to its SQL keyword.
+ */
+static const char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command: %d", (int) cmd);
+	}
+}
+
+/*
+ * pg_get_policy_ddl_internal
+ *		Generate the DDL statement to recreate a row-level security policy.
+ *
+ * Returns a List containing a single palloc'd string with the CREATE POLICY
+ * statement.  Returning a List keeps the calling convention consistent with
+ * the rest of the pg_get_*_ddl family even though only one row is produced.
+ */
+static List *
+pg_get_policy_ddl_internal(Oid tableID, const char *policyName, bool pretty)
+{
+	Relation	pgPolicyRel;
+	HeapTuple	tuplePolicy;
+	Form_pg_policy policyForm;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	StringInfoData buf;
+	Datum		valueDatum;
+	bool		attrIsNull;
+	char	   *targetTable;
+	List	   *statements = NIL;
+
+	/* Validate that the relation exists */
+	{
+		char	   *relname = get_rel_name(tableID);
+		char	   *nspname;
+
+		if (relname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_TABLE),
+					 errmsg("relation with OID %u does not exist", tableID)));
+
+		nspname = get_namespace_name(get_rel_namespace(tableID));
+		if (nspname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_SCHEMA),
+					 errmsg("schema for relation with OID %u does not exist",
+							tableID)));
+
+		targetTable = quote_qualified_identifier(nspname, relname);
+		pfree(relname);
+		pfree(nspname);
+	}
+
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(policyName));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						policyName, targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	initStringInfo(&buf);
+
+	/* Build the CREATE POLICY statement */
+	appendStringInfo(&buf, "CREATE POLICY %s ON %s",
+					 quote_identifier(policyName),
+					 targetTable);
+
+	/*
+	 * Emit AS RESTRICTIVE only when it differs from the default (PERMISSIVE).
+	 */
+	if (!policyForm->polpermissive)
+		append_ddl_option(&buf, pretty, 4, "AS RESTRICTIVE");
+
+	/*
+	 * Emit FOR <cmd> only when it differs from the default (ALL, encoded as
+	 * '*').
+	 */
+	if (policyForm->polcmd != '*')
+		append_ddl_option(&buf, pretty, 4, "FOR %s",
+						  get_policy_cmd_name(policyForm->polcmd));
+
+	/*
+	 * Emit TO <roles> only when it differs from the default (PUBLIC).  PUBLIC
+	 * is encoded in polroles as a single InvalidOid element, so we omit the
+	 * clause whenever every entry is InvalidOid.
+	 */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (role_names.len > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, quote_identifier(rolename));
+			}
+		}
+
+		if (role_names.len > 0)
+			append_ddl_option(&buf, pretty, 4, "TO %s", role_names.data);
+
+		pfree(role_names.data);
+		pfree(policy_roles);
+	}
+
+	/* USING expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		/*
+		 * In non-pretty mode pg_get_expr() already wraps the expression in
+		 * parentheses, so emit it verbatim; in pretty mode we have to add
+		 * the parentheses required by CREATE POLICY syntax ourselves.
+		 */
+		append_ddl_option(&buf, pretty, 4,
+						  pretty ? "USING (%s)" : "USING %s",
+						  TextDatumGetCString(expr));
+	}
+
+	/* WITH CHECK expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		/* See comment above about parenthesization. */
+		append_ddl_option(&buf, pretty, 4,
+						  pretty ? "WITH CHECK (%s)" : "WITH CHECK %s",
+						  TextDatumGetCString(expr));
+	}
+
+	appendStringInfoChar(&buf, ';');
+
+	statements = lappend(statements, pstrdup(buf.data));
+
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+	pfree(buf.data);
+
+	return statements;
+}
+
+/*
+ * pg_get_policy_ddl
+ *		Return DDL to recreate a row-level security policy as a single text row.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	List	   *statements;
+
+	if (SRF_IS_FIRSTCALL())
+	{
+		MemoryContext oldcontext;
+		Oid			tableID;
+		Name		policyName;
+		DdlOption	opts[] = {
+			{"pretty", DDL_OPT_BOOL},
+		};
+
+		funcctx = SRF_FIRSTCALL_INIT();
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+		{
+			MemoryContextSwitchTo(oldcontext);
+			SRF_RETURN_DONE(funcctx);
+		}
+
+		tableID = PG_GETARG_OID(0);
+		policyName = PG_GETARG_NAME(1);
+
+		parse_ddl_options(fcinfo, 2, opts, lengthof(opts));
+
+		statements = pg_get_policy_ddl_internal(tableID,
+												NameStr(*policyName),
+												opts[0].isset && opts[0].boolval);
+		funcctx->user_fctx = statements;
+		funcctx->max_calls = list_length(statements);
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	funcctx = SRF_PERCALL_SETUP();
+	statements = (List *) funcctx->user_fctx;
+
+	if (funcctx->call_cntr < funcctx->max_calls)
+	{
+		char	   *stmt;
+
+		stmt = list_nth(statements, funcctx->call_cntr);
+
+		SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+	}
+	else
+	{
+		list_free_deep(statements);
+		SRF_RETURN_DONE(funcctx);
+	}
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..3e32c89fd6d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,14 @@
   proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
   proargmodes => '{i,v}', proargdefaults => '{NULL}',
   prosrc => 'pg_get_database_ddl' },
+{ oid => '6517', descr => 'get DDL to recreate a row-level security policy',
+  proname => 'pg_get_policy_ddl', prorows => '1', provariadic => 'text',
+  proisstrict => 'f', proretset => 't', provolatile => 's',
+  pronargdefaults => '1', prorettype => 'text',
+  proargtypes => 'regclass name text',
+  proallargtypes => '{regclass,name,text}',
+  proargmodes => '{i,i,v}', proargdefaults => '{NULL}',
+  prosrc => 'pg_get_policy_ddl' },
 { oid => '2509',
   descr => 'deparse an encoded expression with pretty-print option',
   proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 3a5e82c35bd..4e5dd039e91 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -5195,6 +5195,199 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(table, policy_name, VARIADIC options) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+ count 
+-------
+     0
+(1 row)
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+ERROR:  relation "nonexistent_tbl" does not exist
+LINE 1: SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1')...
+                                        ^
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+ERROR:  policy "nonexistent_pol" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Invalid option name
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'badopt', 'true');
+ERROR:  unrecognized option: "badopt"
+-- Invalid boolean value for option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'maybe');
+ERROR:  invalid value for boolean option "pretty": maybe
+-- Duplicate option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true', 'pretty', 'false');
+ERROR:  option "pretty" is specified more than once
+-- Without pretty formatting (default)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+                                       pg_get_policy_ddl                                        
+------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 USING (dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                             +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+                                            pg_get_policy_ddl                                            
+---------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE USING ((cid <> 44) AND (cid < 50));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+                                  pg_get_policy_ddl                                   
+--------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING (dauthor = CURRENT_USER);
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+                                   pg_get_policy_ddl                                    
+----------------------------------------------------------------------------------------
+ CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 FOR SELECT USING ((cid % 2) = 0);
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+                                      pg_get_policy_ddl                                      
+---------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 FOR INSERT WITH CHECK ((cid % 2) = 1);
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'false');
+                                   pg_get_policy_ddl                                    
+----------------------------------------------------------------------------------------
+ CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING ((cid % 2) = 0);
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'false');
+                                pg_get_policy_ddl                                 
+----------------------------------------------------------------------------------
+ CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 FOR DELETE USING (cid < 8);
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'false');
+                                            pg_get_policy_ddl                                            
+---------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING true;
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'false');
+                                                     pg_get_policy_ddl                                                      
+----------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2));
+(1 row)
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+    AS RESTRICTIVE
+    USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+    FOR SELECT
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+    FOR INSERT
+    WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+    FOR UPDATE
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+    FOR DELETE
+    USING (cid < 8);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_dave, regress_rls_alice
+    USING (true);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_exempt_user
+    WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+\pset format aligned
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 6b3566271df..aca6102de71 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2598,6 +2598,93 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(table, policy_name, VARIADIC options) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+
+-- Invalid option name
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'badopt', 'true');
+-- Invalid boolean value for option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'maybe');
+-- Duplicate option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true', 'pretty', 'false');
+
+-- Without pretty formatting (default)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'false');
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'true');
+\pset format aligned
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-05-28 13:41  Ilmar Y <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Ilmar Y @ 2026-05-28 13:41 UTC (permalink / raw)
  To: [email protected]; +Cc: Akshay Joshi <[email protected]>

The following review has been posted through the commitfest application:
make installcheck-world:  not tested
Implements feature:       tested, failed
Spec compliant:           not tested
Documentation:            not tested

Hi,

I looked at v10, focused on whether the generated CREATE POLICY statement
can be executed again.

The patch applies cleanly on current master at
8a86aa313a714adc56c74e4b08793e4e6102b5ca.

git diff --check reports no issues.

I built with:

./configure --prefix="$PWD/pg-install" --without-readline --without-zlib --without-icu
make -s -j8
make -s install

make -C src/test/regress check TESTS=rowsecurity

ended up running the full parallel_schedule in this makefile; all 245 tests
passed, including rowsecurity.

I found one correctness issue in the generated non-pretty DDL.  The code
assumes that pg_get_expr_ext(..., false) already returns the parentheses
required by CREATE POLICY syntax, but that is not true for simple boolean
constants.

For example:

CREATE TABLE t(a int);
CREATE POLICY p_true ON t USING (true);
SELECT ddl FROM pg_get_policy_ddl('t', 'p_true', 'pretty', 'false') AS ddl;

returns:

CREATE POLICY p_true ON public.t USING true;

If I drop the policy and execute that generated statement, it fails:

ERROR:  syntax error at or near "true"
LINE 1: CREATE POLICY p_true ON public.t USING true;
                                               ^

The same issue reproduces for WITH CHECK:

CREATE POLICY p_check ON t FOR INSERT WITH CHECK (false);

is reconstructed as:

CREATE POLICY p_check ON public.t FOR INSERT WITH CHECK false;

and executing it fails at "false".

So I think USING and WITH CHECK need to be parenthesized in non-pretty mode
too, or the tests should include a round-trip execution check for generated
DDL with simple boolean expressions.

I used two small SQL reproducers for the manual checks; the complete repro is
included above.

I have not reviewed the broader pg_get_*_ddl API design or every possible
policy expression form.

Regards,
Ilmar Yunusov

The new status of this patch is: Waiting on Author


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-05-29 06:50  Akshay Joshi <[email protected]>
  parent: Ilmar Y <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2026-05-29 06:50 UTC (permalink / raw)
  To: Ilmar Y <[email protected]>; +Cc: [email protected]

Thanks for the reviews.

My original patch (v9) was actually correct. After considering Japin's
review comment, I initially thought the extra parentheses weren't
necessary, but they are indeed required for handling boolean values
properly in non-pretty mode too, so I kept them in USING (%s) / WITH CHECK
(%s) for both modes.

`pg_get_expr()` only adds outer parentheses for composite expressions (via
the deparsers for `OpExpr`, `BoolExpr`, etc.). For atomic top-level nodes
like `Const`, `Var`, `current_user`, `NULL`, etc.
For example:

    CREATE POLICY p ON t USING (true);
    SELECT pg_get_policy_ddl('t', 'p');  -- previously: ... USING true;
 (syntax error)

This is exactly why `pg_dump` always wraps the expression unconditionally;
see `src/bin/pg_dump/pg_dump.c`:4473-4477:

    if (polinfo->polqual != NULL)
        appendPQExpBuffer(query, " USING (%s)", polinfo->polqual);
    if (polinfo->polwithcheck != NULL)
        appendPQExpBuffer(query, " WITH CHECK (%s)", polinfo->polwithcheck);

I've also added a round-trip regression test with `USING (true)` / `WITH
CHECK (false)` that captures the generated DDL, drops the policies,
re-executes the DDL, and verifies the policies are recreated.

*v11 Patch attached for review.*

On Thu, May 28, 2026 at 7:12 PM Ilmar Y <[email protected]> wrote:

> The following review has been posted through the commitfest application:
> make installcheck-world:  not tested
> Implements feature:       tested, failed
> Spec compliant:           not tested
> Documentation:            not tested
>
> Hi,
>
> I looked at v10, focused on whether the generated CREATE POLICY statement
> can be executed again.
>
> The patch applies cleanly on current master at
> 8a86aa313a714adc56c74e4b08793e4e6102b5ca.
>
> git diff --check reports no issues.
>
> I built with:
>
> ./configure --prefix="$PWD/pg-install" --without-readline --without-zlib
> --without-icu
> make -s -j8
> make -s install
>
> make -C src/test/regress check TESTS=rowsecurity
>
> ended up running the full parallel_schedule in this makefile; all 245 tests
> passed, including rowsecurity.
>
> I found one correctness issue in the generated non-pretty DDL.  The code
> assumes that pg_get_expr_ext(..., false) already returns the parentheses
> required by CREATE POLICY syntax, but that is not true for simple boolean
> constants.
>
> For example:
>
> CREATE TABLE t(a int);
> CREATE POLICY p_true ON t USING (true);
> SELECT ddl FROM pg_get_policy_ddl('t', 'p_true', 'pretty', 'false') AS ddl;
>
> returns:
>
> CREATE POLICY p_true ON public.t USING true;
>
> If I drop the policy and execute that generated statement, it fails:
>
> ERROR:  syntax error at or near "true"
> LINE 1: CREATE POLICY p_true ON public.t USING true;
>                                                ^
>
> The same issue reproduces for WITH CHECK:
>
> CREATE POLICY p_check ON t FOR INSERT WITH CHECK (false);
>
> is reconstructed as:
>
> CREATE POLICY p_check ON public.t FOR INSERT WITH CHECK false;
>
> and executing it fails at "false".
>
> So I think USING and WITH CHECK need to be parenthesized in non-pretty mode
> too, or the tests should include a round-trip execution check for generated
> DDL with simple boolean expressions.
>
> I used two small SQL reproducers for the manual checks; the complete repro
> is
> included above.
>
> I have not reviewed the broader pg_get_*_ddl API design or every possible
> policy expression form.
>
> Regards,
> Ilmar Yunusov
>
> The new status of this patch is: Waiting on Author
>


Attachments:

  [application/x-patch] v11-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch (25.5K, ../../CANxoLDe7xiCWY-UEmzoK_uQdKh3PPNcGXJ3qdzFy3c0o_F+P_Q@mail.gmail.com/3-v11-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From f62ea394cdefed5fe7c6760cceaf2c70f5ec09cc Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 22 May 2026 18:18:07 +0530
Subject: [PATCH v11] Add pg_get_policy_ddl() function to reconstruct CREATE
 POLICY statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduces a new system function:
    pg_get_policy_ddl(table regclass, policy_name name,
       VARIADIC options text[]) RETURNS setof text

which reconstructs the CREATE POLICY statement for the named row-level
security policy on the specified table. The result is returned as a single row.
 
The supported option is:

    pretty (boolean) - format the output for readability.

Usage examples:
    -- non-pretty formatted DDL (default)
    SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1');
    SELECT * FROM pg_get_policy_ddl(16564, 'pol1');

    -- pretty formatted DDL
    SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1', 'pretty', 'true');
    SELECT * FROM pg_get_policy_ddl(16564, 'pol1', 'pretty', 'true');

Reference: PG-163
Author: Akshay Joshi <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  20 ++
 src/backend/utils/adt/ddlutils.c          | 262 ++++++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   8 +
 src/test/regress/expected/rowsecurity.out | 215 ++++++++++++++++++
 src/test/regress/sql/rowsecurity.sql      | 102 +++++++++
 5 files changed, 607 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 00f64f50ceb..44bf6455bb1 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3961,6 +3961,26 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
         <literal>TABLESPACE</literal>.
        </para></entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>,
+        <parameter>policy_name</parameter> <type>name</type>
+        <optional>, <literal>VARIADIC</literal> <parameter>options</parameter>
+        <type>text</type> </optional> )
+        <returnvalue>setof text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement for the
+        named row-level security policy on the specified table.  The result
+        is returned as a single row.
+        The following option is supported: <literal>pretty</literal>
+        (boolean) for formatted output.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c
index f32fcd453ef..728d3648979 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_tablespace.h"
 #include "commands/tablespace.h"
 #include "common/relpath.h"
@@ -86,6 +87,9 @@ static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner
 static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull);
 static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
 										  bool no_owner, bool no_tablespace);
+static List *pg_get_policy_ddl_internal(Oid tableID, const char *policyName,
+										bool pretty);
+static const char *get_policy_cmd_name(char cmd);
 
 
 /*
@@ -1185,3 +1189,261 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
 		SRF_RETURN_DONE(funcctx);
 	}
 }
+
+/*
+ * get_policy_cmd_name
+ *		Map a pg_policy.polcmd char to its SQL keyword.
+ */
+static const char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command: %d", (int) cmd);
+	}
+}
+
+/*
+ * pg_get_policy_ddl_internal
+ *		Generate the DDL statement to recreate a row-level security policy.
+ *
+ * Returns a List containing a single palloc'd string with the CREATE POLICY
+ * statement.  Returning a List keeps the calling convention consistent with
+ * the rest of the pg_get_*_ddl family even though only one row is produced.
+ */
+static List *
+pg_get_policy_ddl_internal(Oid tableID, const char *policyName, bool pretty)
+{
+	Relation	pgPolicyRel;
+	HeapTuple	tuplePolicy;
+	Form_pg_policy policyForm;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	StringInfoData buf;
+	Datum		valueDatum;
+	bool		attrIsNull;
+	char	   *targetTable;
+	List	   *statements = NIL;
+
+	/* Validate that the relation exists */
+	{
+		char	   *relname = get_rel_name(tableID);
+		char	   *nspname;
+
+		if (relname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_TABLE),
+					 errmsg("relation with OID %u does not exist", tableID)));
+
+		nspname = get_namespace_name(get_rel_namespace(tableID));
+		if (nspname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_SCHEMA),
+					 errmsg("schema for relation with OID %u does not exist",
+							tableID)));
+
+		targetTable = quote_qualified_identifier(nspname, relname);
+		pfree(relname);
+		pfree(nspname);
+	}
+
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(policyName));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						policyName, targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	initStringInfo(&buf);
+
+	/* Build the CREATE POLICY statement */
+	appendStringInfo(&buf, "CREATE POLICY %s ON %s",
+					 quote_identifier(policyName),
+					 targetTable);
+
+	/*
+	 * Emit AS RESTRICTIVE only when it differs from the default (PERMISSIVE).
+	 */
+	if (!policyForm->polpermissive)
+		append_ddl_option(&buf, pretty, 4, "AS RESTRICTIVE");
+
+	/*
+	 * Emit FOR <cmd> only when it differs from the default (ALL, encoded as
+	 * '*').
+	 */
+	if (policyForm->polcmd != '*')
+		append_ddl_option(&buf, pretty, 4, "FOR %s",
+						  get_policy_cmd_name(policyForm->polcmd));
+
+	/*
+	 * Emit TO <roles> only when it differs from the default (PUBLIC).  PUBLIC
+	 * is encoded in polroles as a single InvalidOid element, so we omit the
+	 * clause whenever every entry is InvalidOid.
+	 */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (role_names.len > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, quote_identifier(rolename));
+			}
+		}
+
+		if (role_names.len > 0)
+			append_ddl_option(&buf, pretty, 4, "TO %s", role_names.data);
+
+		pfree(role_names.data);
+		pfree(policy_roles);
+	}
+
+	/* USING expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "USING (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	/* WITH CHECK expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "WITH CHECK (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	appendStringInfoChar(&buf, ';');
+
+	statements = lappend(statements, pstrdup(buf.data));
+
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+	pfree(buf.data);
+
+	return statements;
+}
+
+/*
+ * pg_get_policy_ddl
+ *		Return DDL to recreate a row-level security policy as a single text row.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	List	   *statements;
+
+	if (SRF_IS_FIRSTCALL())
+	{
+		MemoryContext oldcontext;
+		Oid			tableID;
+		Name		policyName;
+		DdlOption	opts[] = {
+			{"pretty", DDL_OPT_BOOL},
+		};
+
+		funcctx = SRF_FIRSTCALL_INIT();
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+		{
+			MemoryContextSwitchTo(oldcontext);
+			SRF_RETURN_DONE(funcctx);
+		}
+
+		tableID = PG_GETARG_OID(0);
+		policyName = PG_GETARG_NAME(1);
+
+		parse_ddl_options(fcinfo, 2, opts, lengthof(opts));
+
+		statements = pg_get_policy_ddl_internal(tableID,
+												NameStr(*policyName),
+												opts[0].isset && opts[0].boolval);
+		funcctx->user_fctx = statements;
+		funcctx->max_calls = list_length(statements);
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	funcctx = SRF_PERCALL_SETUP();
+	statements = (List *) funcctx->user_fctx;
+
+	if (funcctx->call_cntr < funcctx->max_calls)
+	{
+		char	   *stmt;
+
+		stmt = list_nth(statements, funcctx->call_cntr);
+
+		SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+	}
+	else
+	{
+		list_free_deep(statements);
+		SRF_RETURN_DONE(funcctx);
+	}
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..3e32c89fd6d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,14 @@
   proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
   proargmodes => '{i,v}', proargdefaults => '{NULL}',
   prosrc => 'pg_get_database_ddl' },
+{ oid => '6517', descr => 'get DDL to recreate a row-level security policy',
+  proname => 'pg_get_policy_ddl', prorows => '1', provariadic => 'text',
+  proisstrict => 'f', proretset => 't', provolatile => 's',
+  pronargdefaults => '1', prorettype => 'text',
+  proargtypes => 'regclass name text',
+  proallargtypes => '{regclass,name,text}',
+  proargmodes => '{i,i,v}', proargdefaults => '{NULL}',
+  prosrc => 'pg_get_policy_ddl' },
 { oid => '2509',
   descr => 'deparse an encoded expression with pretty-print option',
   proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 3a5e82c35bd..502f9b86f64 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -5195,6 +5195,221 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(table, policy_name, VARIADIC options) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+ count 
+-------
+     0
+(1 row)
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+ERROR:  relation "nonexistent_tbl" does not exist
+LINE 1: SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1')...
+                                        ^
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+ERROR:  policy "nonexistent_pol" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Invalid option name
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'badopt', 'true');
+ERROR:  unrecognized option: "badopt"
+-- Invalid boolean value for option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'maybe');
+ERROR:  invalid value for boolean option "pretty": maybe
+-- Duplicate option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true', 'pretty', 'false');
+ERROR:  option "pretty" is specified more than once
+-- Without pretty formatting (default)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                              +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+                                   pg_get_policy_ddl                                    
+----------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 FOR SELECT USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+                                       pg_get_policy_ddl                                       
+-----------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 FOR INSERT WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'false');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'false');
+                                 pg_get_policy_ddl                                  
+------------------------------------------------------------------------------------
+ CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 FOR DELETE USING ((cid < 8));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'false');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'false');
+                                                      pg_get_policy_ddl                                                      
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 TO regress_rls_exempt_user WITH CHECK ((cid = ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2)));
+(1 row)
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+    AS RESTRICTIVE
+    USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+    FOR SELECT
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+    FOR INSERT
+    WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+    FOR UPDATE
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+    FOR DELETE
+    USING (cid < 8);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_dave, regress_rls_alice
+    USING (true);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_exempt_user
+    WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+\pset format aligned
+-- Round-trip: the generated DDL must be re-executable, including for atomic
+-- boolean expressions that pg_get_expr() does not parenthesize.
+CREATE TABLE rls_rt (a int);
+CREATE POLICY rt_true ON rls_rt USING (true);
+CREATE POLICY rt_false ON rls_rt FOR INSERT WITH CHECK (false);
+CREATE TEMP TABLE rt_ddl AS
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_true') AS ddl
+    UNION ALL
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_false');
+DROP POLICY rt_true ON rls_rt;
+DROP POLICY rt_false ON rls_rt;
+SELECT ddl FROM rt_ddl ORDER BY ddl \gexec
+CREATE POLICY rt_false ON regress_rls_schema.rls_rt FOR INSERT WITH CHECK (false);
+CREATE POLICY rt_true ON regress_rls_schema.rls_rt USING (true);
+SELECT polname FROM pg_policy WHERE polrelid = 'rls_rt'::regclass ORDER BY polname;
+ polname  
+----------
+ rt_false
+ rt_true
+(2 rows)
+
+DROP TABLE rls_rt;
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 6b3566271df..d3b2e226d07 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2598,6 +2598,108 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(table, policy_name, VARIADIC options) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+
+-- Invalid option name
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'badopt', 'true');
+-- Invalid boolean value for option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'maybe');
+-- Duplicate option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true', 'pretty', 'false');
+
+-- Without pretty formatting (default)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'false');
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'true');
+\pset format aligned
+
+-- Round-trip: the generated DDL must be re-executable, including for atomic
+-- boolean expressions that pg_get_expr() does not parenthesize.
+CREATE TABLE rls_rt (a int);
+CREATE POLICY rt_true ON rls_rt USING (true);
+CREATE POLICY rt_false ON rls_rt FOR INSERT WITH CHECK (false);
+CREATE TEMP TABLE rt_ddl AS
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_true') AS ddl
+    UNION ALL
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_false');
+DROP POLICY rt_true ON rls_rt;
+DROP POLICY rt_false ON rls_rt;
+SELECT ddl FROM rt_ddl ORDER BY ddl \gexec
+SELECT polname FROM pg_policy WHERE polrelid = 'rls_rt'::regclass ORDER BY polname;
+DROP TABLE rls_rt;
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-05-29 07:38  Japin Li <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 2 replies; 44+ messages in thread

From: Japin Li @ 2026-05-29 07:38 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Ilmar Y <[email protected]>; [email protected]

On Fri, 29 May 2026 at 12:20, Akshay Joshi <[email protected]> wrote:
> Thanks for the reviews. 
>
> My original patch (v9) was actually correct. After considering Japin's review comment, I initially thought the extra
> parentheses weren't necessary, but they are indeed required for handling boolean values properly in non-pretty mode too,
> so I kept them in USING (%s) / WITH CHECK (%s) for both modes.
>

My bad!  I had not considered this situation.

> `pg_get_expr()` only adds outer parentheses for composite expressions (via the deparsers for `OpExpr`, `BoolExpr`, etc.).
> For atomic top-level nodes like `Const`, `Var`, `current_user`, `NULL`, etc. 
> For example:
>
>     CREATE POLICY p ON t USING (true);
>     SELECT pg_get_policy_ddl('t', 'p');  -- previously: ... USING true;  (syntax error)
>
> This is exactly why `pg_dump` always wraps the expression unconditionally; see `src/bin/pg_dump/pg_dump.c`:4473-4477:
>
>     if (polinfo->polqual != NULL)
>         appendPQExpBuffer(query, " USING (%s)", polinfo->polqual);
>     if (polinfo->polwithcheck != NULL)
>         appendPQExpBuffer(query, " WITH CHECK (%s)", polinfo->polwithcheck);
>
> I've also added a round-trip regression test with `USING (true)` / `WITH CHECK (false)` that captures the generated DDL,
> drops the policies, re-executes the DDL, and verifies the policies are recreated. 
>
> v11 Patch attached for review.
>
> On Thu, May 28, 2026 at 7:12 PM Ilmar Y <[email protected]> wrote:
>
>  The following review has been posted through the commitfest application:
>  make installcheck-world:  not tested
>  Implements feature:       tested, failed
>  Spec compliant:           not tested
>  Documentation:            not tested
>
>  Hi,
>
>  I looked at v10, focused on whether the generated CREATE POLICY statement
>  can be executed again.
>
>  The patch applies cleanly on current master at
>  8a86aa313a714adc56c74e4b08793e4e6102b5ca.
>
>  git diff --check reports no issues.
>
>  I built with:
>
>  ./configure --prefix="$PWD/pg-install" --without-readline --without-zlib --without-icu
>  make -s -j8
>  make -s install
>
>  make -C src/test/regress check TESTS=rowsecurity
>
>  ended up running the full parallel_schedule in this makefile; all 245 tests
>  passed, including rowsecurity.
>
>  I found one correctness issue in the generated non-pretty DDL.  The code
>  assumes that pg_get_expr_ext(..., false) already returns the parentheses
>  required by CREATE POLICY syntax, but that is not true for simple boolean
>  constants.
>
>  For example:
>
>  CREATE TABLE t(a int);
>  CREATE POLICY p_true ON t USING (true);
>  SELECT ddl FROM pg_get_policy_ddl('t', 'p_true', 'pretty', 'false') AS ddl;
>
>  returns:
>
>  CREATE POLICY p_true ON public.t USING true;
>
>  If I drop the policy and execute that generated statement, it fails:
>
>  ERROR:  syntax error at or near "true"
>  LINE 1: CREATE POLICY p_true ON public.t USING true;
>                                                 ^
>
>  The same issue reproduces for WITH CHECK:
>
>  CREATE POLICY p_check ON t FOR INSERT WITH CHECK (false);
>
>  is reconstructed as:
>
>  CREATE POLICY p_check ON public.t FOR INSERT WITH CHECK false;
>
>  and executing it fails at "false".
>
>  So I think USING and WITH CHECK need to be parenthesized in non-pretty mode
>  too, or the tests should include a round-trip execution check for generated
>  DDL with simple boolean expressions.
>
>  I used two small SQL reproducers for the manual checks; the complete repro is
>  included above.
>
>  I have not reviewed the broader pg_get_*_ddl API design or every possible
>  policy expression form.
>
>  Regards,
>  Ilmar Yunusov
>
>  The new status of this patch is: Waiting on Author

-- 
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.






^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-05-30 12:14  Ilmar Y <[email protected]>
  parent: Japin Li <[email protected]>
  1 sibling, 0 replies; 44+ messages in thread

From: Ilmar Y @ 2026-05-30 12:14 UTC (permalink / raw)
  To: [email protected]; +Cc: Akshay Joshi <[email protected]>

The following review has been posted through the commitfest application:
make installcheck-world:  not tested
Implements feature:       tested, passed
Spec compliant:           not tested
Documentation:            not tested

Hi,

I re-checked v11 against current origin/master at
db5ed03217b9c238703df8b4b286115d6e940488.

The patch applies cleanly, and git diff --check reports no issues.

I built with:

./configure --prefix="$PWD/pg-install" --without-readline --without-zlib --without-icu
make -s -j8
make -s install

make -C src/test/regress check TESTS=rowsecurity

passed. The regression run completed successfully; all 245 tests passed,
including rowsecurity.

I also re-ran the two manual repro scripts from my previous review. Both now
complete successfully.

The non-pretty USING case now reconstructs:

CREATE POLICY p_true ON public.t USING (true);

and the generated statement executes successfully.

The non-pretty WITH CHECK case now reconstructs:

CREATE POLICY p_check ON public.t FOR INSERT WITH CHECK (false);

and that generated statement also executes successfully.

So the round-trip issue I reported for v10 is fixed for me in v11.

Regards,
Ilmar Yunusov

^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-06-08 12:41  solai v <[email protected]>
  parent: Japin Li <[email protected]>
  1 sibling, 1 reply; 44+ messages in thread

From: solai v @ 2026-06-08 12:41 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Ilmar Y <[email protected]>; [email protected]

Hi all,


On Mon, Jun 8, 2026 at 5:32 PM Japin Li <[email protected]> wrote:
>
> On Fri, 29 May 2026 at 12:20, Akshay Joshi <[email protected]> wrote:
> > Thanks for the reviews.
> >
> > My original patch (v9) was actually correct. After considering Japin's review comment, I initially thought the extra
> > parentheses weren't necessary, but they are indeed required for handling boolean values properly in non-pretty mode too,
> > so I kept them in USING (%s) / WITH CHECK (%s) for both modes.
> >
>
> My bad!  I had not considered this situation.
>
> > `pg_get_expr()` only adds outer parentheses for composite expressions (via the deparsers for `OpExpr`, `BoolExpr`, etc.).
> > For atomic top-level nodes like `Const`, `Var`, `current_user`, `NULL`, etc.
> > For example:
> >
> >     CREATE POLICY p ON t USING (true);
> >     SELECT pg_get_policy_ddl('t', 'p');  -- previously: ... USING true;  (syntax error)
> >
> > This is exactly why `pg_dump` always wraps the expression unconditionally; see `src/bin/pg_dump/pg_dump.c`:4473-4477:
> >
> >     if (polinfo->polqual != NULL)
> >         appendPQExpBuffer(query, " USING (%s)", polinfo->polqual);
> >     if (polinfo->polwithcheck != NULL)
> >         appendPQExpBuffer(query, " WITH CHECK (%s)", polinfo->polwithcheck);
> >
> > I've also added a round-trip regression test with `USING (true)` / `WITH CHECK (false)` that captures the generated DDL,
> > drops the policies, re-executes the DDL, and verifies the policies are recreated.
> >
> > v11 Patch attached for review.
> >
> > On Thu, May 28, 2026 at 7:12 PM Ilmar Y <[email protected]> wrote:
> >
> >  The following review has been posted through the commitfest application:
> >  make installcheck-world:  not tested
> >  Implements feature:       tested, failed
> >  Spec compliant:           not tested
> >  Documentation:            not tested
> >
> >  Hi,
> >
> >  I looked at v10, focused on whether the generated CREATE POLICY statement
> >  can be executed again.
> >
> >  The patch applies cleanly on current master at
> >  8a86aa313a714adc56c74e4b08793e4e6102b5ca.
> >
> >  git diff --check reports no issues.
> >
> >  I built with:
> >
> >  ./configure --prefix="$PWD/pg-install" --without-readline --without-zlib --without-icu
> >  make -s -j8
> >  make -s install
> >
> >  make -C src/test/regress check TESTS=rowsecurity
> >
> >  ended up running the full parallel_schedule in this makefile; all 245 tests
> >  passed, including rowsecurity.
> >
> >  I found one correctness issue in the generated non-pretty DDL.  The code
> >  assumes that pg_get_expr_ext(..., false) already returns the parentheses
> >  required by CREATE POLICY syntax, but that is not true for simple boolean
> >  constants.
> >
> >  For example:
> >
> >  CREATE TABLE t(a int);
> >  CREATE POLICY p_true ON t USING (true);
> >  SELECT ddl FROM pg_get_policy_ddl('t', 'p_true', 'pretty', 'false') AS ddl;
> >
> >  returns:
> >
> >  CREATE POLICY p_true ON public.t USING true;
> >
> >  If I drop the policy and execute that generated statement, it fails:
> >
> >  ERROR:  syntax error at or near "true"
> >  LINE 1: CREATE POLICY p_true ON public.t USING true;
> >                                                 ^
> >
> >  The same issue reproduces for WITH CHECK:
> >
> >  CREATE POLICY p_check ON t FOR INSERT WITH CHECK (false);
> >
> >  is reconstructed as:
> >
> >  CREATE POLICY p_check ON public.t FOR INSERT WITH CHECK false;
> >
> >  and executing it fails at "false".
> >
> >  So I think USING and WITH CHECK need to be parenthesized in non-pretty mode
> >  too, or the tests should include a round-trip execution check for generated
> >  DDL with simple boolean expressions.
> >
> >  I used two small SQL reproducers for the manual checks; the complete repro is
> >  included above.
> >
> >  I have not reviewed the broader pg_get_*_ddl API design or every possible
> >  policy expression form.
> >
> >  Regards,
> >  Ilmar Yunusov
> >
> >  The new status of this patch is: Waiting on Author
>


I reviewed and tested the V11 pg_get_policy_ddl() patch. I verified
the basic functionality by creating various row-level security
policies and checking the reconstructed DDL returned by
pg_get_policy_ddl(). The function correctly reconstructed policies for
different command types (SELECT, INSERT, UPDATE, DELETE), PERMISSIVE
and RESTRICTIVE policies, multiple role lists, quoted identifiers,
USING clauses, and WITH CHECK clauses.
I also tested more complex cases involving subqueries. Policies
containing EXISTS subqueries in both USING and WITH CHECK clauses were
reconstructed successfully. Nested subqueries were also handled
correctly, and I did not encounter any issues related to expression
reconstruction or variable handling. I verified NULL input handling as
well. Passing NULL values for the table name or policy name returned
no rows as expected. Invalid relation names resulted in the expected
regclass resolution error. One observation from testing is that the
generated DDL omits default clauses such as FOR ALL and TO PUBLIC. For
example, a policy created with FOR ALL TO PUBLIC is reconstructed
without those clauses, while still producing semantically equivalent
DDL. I'm not sure whether this is intentional or if the function is
expected to reproduce all clauses explicitly, but it may be worth
discussing.
Overall, the patch worked as expected in all scenarios I tested, and I
did not find any functional issues.


Regards,
Solai






^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-06-22 13:04  Akshay Joshi <[email protected]>
  parent: solai v <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2026-06-22 13:04 UTC (permalink / raw)
  To: solai v <[email protected]>; +Cc: Ilmar Y <[email protected]>; [email protected]

On Mon, Jun 8, 2026 at 6:10 PM solai v <[email protected]> wrote:

> Hi all,
>
>
> On Mon, Jun 8, 2026 at 5:32 PM Japin Li <[email protected]> wrote:
> >
> > On Fri, 29 May 2026 at 12:20, Akshay Joshi <
> [email protected]> wrote:
> > > Thanks for the reviews.
> > >
> > > My original patch (v9) was actually correct. After considering Japin's
> review comment, I initially thought the extra
> > > parentheses weren't necessary, but they are indeed required for
> handling boolean values properly in non-pretty mode too,
> > > so I kept them in USING (%s) / WITH CHECK (%s) for both modes.
> > >
> >
> > My bad!  I had not considered this situation.
> >
> > > `pg_get_expr()` only adds outer parentheses for composite expressions
> (via the deparsers for `OpExpr`, `BoolExpr`, etc.).
> > > For atomic top-level nodes like `Const`, `Var`, `current_user`,
> `NULL`, etc.
> > > For example:
> > >
> > >     CREATE POLICY p ON t USING (true);
> > >     SELECT pg_get_policy_ddl('t', 'p');  -- previously: ... USING
> true;  (syntax error)
> > >
> > > This is exactly why `pg_dump` always wraps the expression
> unconditionally; see `src/bin/pg_dump/pg_dump.c`:4473-4477:
> > >
> > >     if (polinfo->polqual != NULL)
> > >         appendPQExpBuffer(query, " USING (%s)", polinfo->polqual);
> > >     if (polinfo->polwithcheck != NULL)
> > >         appendPQExpBuffer(query, " WITH CHECK (%s)",
> polinfo->polwithcheck);
> > >
> > > I've also added a round-trip regression test with `USING (true)` /
> `WITH CHECK (false)` that captures the generated DDL,
> > > drops the policies, re-executes the DDL, and verifies the policies are
> recreated.
> > >
> > > v11 Patch attached for review.
> > >
> > > On Thu, May 28, 2026 at 7:12 PM Ilmar Y <[email protected]> wrote:
> > >
> > >  The following review has been posted through the commitfest
> application:
> > >  make installcheck-world:  not tested
> > >  Implements feature:       tested, failed
> > >  Spec compliant:           not tested
> > >  Documentation:            not tested
> > >
> > >  Hi,
> > >
> > >  I looked at v10, focused on whether the generated CREATE POLICY
> statement
> > >  can be executed again.
> > >
> > >  The patch applies cleanly on current master at
> > >  8a86aa313a714adc56c74e4b08793e4e6102b5ca.
> > >
> > >  git diff --check reports no issues.
> > >
> > >  I built with:
> > >
> > >  ./configure --prefix="$PWD/pg-install" --without-readline
> --without-zlib --without-icu
> > >  make -s -j8
> > >  make -s install
> > >
> > >  make -C src/test/regress check TESTS=rowsecurity
> > >
> > >  ended up running the full parallel_schedule in this makefile; all 245
> tests
> > >  passed, including rowsecurity.
> > >
> > >  I found one correctness issue in the generated non-pretty DDL.  The
> code
> > >  assumes that pg_get_expr_ext(..., false) already returns the
> parentheses
> > >  required by CREATE POLICY syntax, but that is not true for simple
> boolean
> > >  constants.
> > >
> > >  For example:
> > >
> > >  CREATE TABLE t(a int);
> > >  CREATE POLICY p_true ON t USING (true);
> > >  SELECT ddl FROM pg_get_policy_ddl('t', 'p_true', 'pretty', 'false')
> AS ddl;
> > >
> > >  returns:
> > >
> > >  CREATE POLICY p_true ON public.t USING true;
> > >
> > >  If I drop the policy and execute that generated statement, it fails:
> > >
> > >  ERROR:  syntax error at or near "true"
> > >  LINE 1: CREATE POLICY p_true ON public.t USING true;
> > >                                                 ^
> > >
> > >  The same issue reproduces for WITH CHECK:
> > >
> > >  CREATE POLICY p_check ON t FOR INSERT WITH CHECK (false);
> > >
> > >  is reconstructed as:
> > >
> > >  CREATE POLICY p_check ON public.t FOR INSERT WITH CHECK false;
> > >
> > >  and executing it fails at "false".
> > >
> > >  So I think USING and WITH CHECK need to be parenthesized in
> non-pretty mode
> > >  too, or the tests should include a round-trip execution check for
> generated
> > >  DDL with simple boolean expressions.
> > >
> > >  I used two small SQL reproducers for the manual checks; the complete
> repro is
> > >  included above.
> > >
> > >  I have not reviewed the broader pg_get_*_ddl API design or every
> possible
> > >  policy expression form.
> > >
> > >  Regards,
> > >  Ilmar Yunusov
> > >
> > >  The new status of this patch is: Waiting on Author
> >
>
>
> I reviewed and tested the V11 pg_get_policy_ddl() patch. I verified
> the basic functionality by creating various row-level security
> policies and checking the reconstructed DDL returned by
> pg_get_policy_ddl(). The function correctly reconstructed policies for
> different command types (SELECT, INSERT, UPDATE, DELETE), PERMISSIVE
> and RESTRICTIVE policies, multiple role lists, quoted identifiers,
> USING clauses, and WITH CHECK clauses.
> I also tested more complex cases involving subqueries. Policies
> containing EXISTS subqueries in both USING and WITH CHECK clauses were
> reconstructed successfully. Nested subqueries were also handled
> correctly, and I did not encounter any issues related to expression
> reconstruction or variable handling. I verified NULL input handling as
> well. Passing NULL values for the table name or policy name returned
> no rows as expected. Invalid relation names resulted in the expected
> regclass resolution error. One observation from testing is that the
> generated DDL omits default clauses such as FOR ALL and TO PUBLIC. For
> example, a policy created with FOR ALL TO PUBLIC is reconstructed
> without those clauses, while still producing semantically equivalent
> DDL. I'm not sure whether this is intentional or if the function is
> expected to reproduce all clauses explicitly, but it may be worth
> discussing.
> Overall, the patch worked as expected in all scenarios I tested, and I
> did not find any functional issues.
>

Thanks for the review. The omission of FOR ALL and TO PUBLIC is intentional
and matches the long-standing convention used by pg_get_indexdef,
pg_get_constraintdef, pg_get_viewdef, etc.: emit a clause only when it
differs from the parser's default. The result is semantically equivalent to
the CREATE POLICY DDL.

>
>
> Regards,
> Solai
>


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-06-22 13:19  Akshay Joshi <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2026-06-22 13:19 UTC (permalink / raw)
  To: solai v <[email protected]>; +Cc: Ilmar Y <[email protected]>; [email protected]

*Chao Li* found an issue in my other patch for *pg_get_table_ddl.*
Specifically,
the existing pattern in pg_proc.dat for variadic-text functions (e.g.,
jsonb_delete, json_extract_path) uses *_text* instead of *text* at the
variadic position in both proargtypes and proallargtypes, with provariadic
=> 'text'. This is the convention documented by the sanity check in
src/test/regress/sql/opr_sanity.sql.

I realized the same issue was present in this patch as well, so I have
fixed it and added corresponding test cases.

The v12 patch is now ready for review and commit.

On Mon, Jun 22, 2026 at 6:34 PM Akshay Joshi <[email protected]>
wrote:

>
>
> On Mon, Jun 8, 2026 at 6:10 PM solai v <[email protected]> wrote:
>
>> Hi all,
>>
>>
>> On Mon, Jun 8, 2026 at 5:32 PM Japin Li <[email protected]> wrote:
>> >
>> > On Fri, 29 May 2026 at 12:20, Akshay Joshi <
>> [email protected]> wrote:
>> > > Thanks for the reviews.
>> > >
>> > > My original patch (v9) was actually correct. After considering
>> Japin's review comment, I initially thought the extra
>> > > parentheses weren't necessary, but they are indeed required for
>> handling boolean values properly in non-pretty mode too,
>> > > so I kept them in USING (%s) / WITH CHECK (%s) for both modes.
>> > >
>> >
>> > My bad!  I had not considered this situation.
>> >
>> > > `pg_get_expr()` only adds outer parentheses for composite expressions
>> (via the deparsers for `OpExpr`, `BoolExpr`, etc.).
>> > > For atomic top-level nodes like `Const`, `Var`, `current_user`,
>> `NULL`, etc.
>> > > For example:
>> > >
>> > >     CREATE POLICY p ON t USING (true);
>> > >     SELECT pg_get_policy_ddl('t', 'p');  -- previously: ... USING
>> true;  (syntax error)
>> > >
>> > > This is exactly why `pg_dump` always wraps the expression
>> unconditionally; see `src/bin/pg_dump/pg_dump.c`:4473-4477:
>> > >
>> > >     if (polinfo->polqual != NULL)
>> > >         appendPQExpBuffer(query, " USING (%s)", polinfo->polqual);
>> > >     if (polinfo->polwithcheck != NULL)
>> > >         appendPQExpBuffer(query, " WITH CHECK (%s)",
>> polinfo->polwithcheck);
>> > >
>> > > I've also added a round-trip regression test with `USING (true)` /
>> `WITH CHECK (false)` that captures the generated DDL,
>> > > drops the policies, re-executes the DDL, and verifies the policies
>> are recreated.
>> > >
>> > > v11 Patch attached for review.
>> > >
>> > > On Thu, May 28, 2026 at 7:12 PM Ilmar Y <[email protected]> wrote:
>> > >
>> > >  The following review has been posted through the commitfest
>> application:
>> > >  make installcheck-world:  not tested
>> > >  Implements feature:       tested, failed
>> > >  Spec compliant:           not tested
>> > >  Documentation:            not tested
>> > >
>> > >  Hi,
>> > >
>> > >  I looked at v10, focused on whether the generated CREATE POLICY
>> statement
>> > >  can be executed again.
>> > >
>> > >  The patch applies cleanly on current master at
>> > >  8a86aa313a714adc56c74e4b08793e4e6102b5ca.
>> > >
>> > >  git diff --check reports no issues.
>> > >
>> > >  I built with:
>> > >
>> > >  ./configure --prefix="$PWD/pg-install" --without-readline
>> --without-zlib --without-icu
>> > >  make -s -j8
>> > >  make -s install
>> > >
>> > >  make -C src/test/regress check TESTS=rowsecurity
>> > >
>> > >  ended up running the full parallel_schedule in this makefile; all
>> 245 tests
>> > >  passed, including rowsecurity.
>> > >
>> > >  I found one correctness issue in the generated non-pretty DDL.  The
>> code
>> > >  assumes that pg_get_expr_ext(..., false) already returns the
>> parentheses
>> > >  required by CREATE POLICY syntax, but that is not true for simple
>> boolean
>> > >  constants.
>> > >
>> > >  For example:
>> > >
>> > >  CREATE TABLE t(a int);
>> > >  CREATE POLICY p_true ON t USING (true);
>> > >  SELECT ddl FROM pg_get_policy_ddl('t', 'p_true', 'pretty', 'false')
>> AS ddl;
>> > >
>> > >  returns:
>> > >
>> > >  CREATE POLICY p_true ON public.t USING true;
>> > >
>> > >  If I drop the policy and execute that generated statement, it fails:
>> > >
>> > >  ERROR:  syntax error at or near "true"
>> > >  LINE 1: CREATE POLICY p_true ON public.t USING true;
>> > >                                                 ^
>> > >
>> > >  The same issue reproduces for WITH CHECK:
>> > >
>> > >  CREATE POLICY p_check ON t FOR INSERT WITH CHECK (false);
>> > >
>> > >  is reconstructed as:
>> > >
>> > >  CREATE POLICY p_check ON public.t FOR INSERT WITH CHECK false;
>> > >
>> > >  and executing it fails at "false".
>> > >
>> > >  So I think USING and WITH CHECK need to be parenthesized in
>> non-pretty mode
>> > >  too, or the tests should include a round-trip execution check for
>> generated
>> > >  DDL with simple boolean expressions.
>> > >
>> > >  I used two small SQL reproducers for the manual checks; the complete
>> repro is
>> > >  included above.
>> > >
>> > >  I have not reviewed the broader pg_get_*_ddl API design or every
>> possible
>> > >  policy expression form.
>> > >
>> > >  Regards,
>> > >  Ilmar Yunusov
>> > >
>> > >  The new status of this patch is: Waiting on Author
>> >
>>
>>
>> I reviewed and tested the V11 pg_get_policy_ddl() patch. I verified
>> the basic functionality by creating various row-level security
>> policies and checking the reconstructed DDL returned by
>> pg_get_policy_ddl(). The function correctly reconstructed policies for
>> different command types (SELECT, INSERT, UPDATE, DELETE), PERMISSIVE
>> and RESTRICTIVE policies, multiple role lists, quoted identifiers,
>> USING clauses, and WITH CHECK clauses.
>> I also tested more complex cases involving subqueries. Policies
>> containing EXISTS subqueries in both USING and WITH CHECK clauses were
>> reconstructed successfully. Nested subqueries were also handled
>> correctly, and I did not encounter any issues related to expression
>> reconstruction or variable handling. I verified NULL input handling as
>> well. Passing NULL values for the table name or policy name returned
>> no rows as expected. Invalid relation names resulted in the expected
>> regclass resolution error. One observation from testing is that the
>> generated DDL omits default clauses such as FOR ALL and TO PUBLIC. For
>> example, a policy created with FOR ALL TO PUBLIC is reconstructed
>> without those clauses, while still producing semantically equivalent
>> DDL. I'm not sure whether this is intentional or if the function is
>> expected to reproduce all clauses explicitly, but it may be worth
>> discussing.
>> Overall, the patch worked as expected in all scenarios I tested, and I
>> did not find any functional issues.
>>
>
> Thanks for the review. The omission of FOR ALL and TO PUBLIC is
> intentional and matches the long-standing convention used by
> pg_get_indexdef, pg_get_constraintdef, pg_get_viewdef, etc.: emit a clause
> only when it differs from the parser's default. The result is semantically
> equivalent to the CREATE POLICY DDL.
>
>>
>>
>> Regards,
>> Solai
>>
>


Attachments:

  [application/octet-stream] v12-0001-Add-pg_get_policy_ddl-to-reconstruct-CREATE.patch (31.1K, ../../CANxoLDf80gXFLSqWM7d-idA=Grot49HsmP9YXZw49nCFe+Mi+g@mail.gmail.com/3-v12-0001-Add-pg_get_policy_ddl-to-reconstruct-CREATE.patch)
  download | inline diff:
From e1d0b654b6517df2270c3a83fe64f6dcba3c0821 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 22 May 2026 18:18:07 +0530
Subject: [PATCH v12] Add pg_get_policy_ddl() to reconstruct CREATE POLICY
 statements

This patch introduces a new system function:

  pg_get_policy_ddl(table regclass,
                    policy_name name,
                    VARIADIC options text[])
  RETURNS SETOF text

which reconstructs the CREATE POLICY statement for the named row-level
security policy on the specified table.  Although a single statement is
produced, the function returns a SETOF text result so its calling
convention matches the rest of the pg_get_*_ddl family.

The reconstructed DDL includes the policy's permissiveness (AS
RESTRICTIVE), command (FOR SELECT/INSERT/UPDATE/DELETE), role list (TO
<roles>), USING qualification, and WITH CHECK expression.  Clauses
whose value equals the parser's default -- PERMISSIVE, FOR ALL, and
TO PUBLIC -- are omitted from the output, matching the convention used
by pg_get_indexdef, pg_get_constraintdef, pg_get_viewdef, and similar
functions.  The result is therefore semantically equivalent to the
original DDL, not lexically identical.

Options are passed as alternating name/value text pairs via the
VARIADIC argument; the supported option is:

  pretty (boolean) -- format the output for readability.

NULL inputs for the table or policy name yield no rows.  An invalid
relation name surfaces the regclass resolution error; a non-existent
policy raises an explicit "policy ... does not exist" error.  Option
parsing rejects unrecognized names, invalid boolean values, duplicate
options, and an odd number of variadic arguments.

Usage examples:

  -- compact form (default)
  SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1');
  SELECT * FROM pg_get_policy_ddl(16564, 'pol1');

  -- pretty-printed form
  SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1',
                                  'pretty', 'true');

  -- options supplied as a constructed array
  SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1',
                                  VARIADIC ARRAY['pretty','true']);

Regression coverage is added to src/test/regress/sql/rowsecurity.sql
and exercises PERMISSIVE/RESTRICTIVE policies, all FOR command
variants, multi-role TO lists, subquery expressions in USING and
WITH CHECK, pretty and non-pretty output, NULL and error paths, the
VARIADIC ARRAY[...] call syntax, and a round-trip test that drops
and re-executes the generated DDL.

Author: Akshay Joshi <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  20 ++
 src/backend/utils/adt/ddlutils.c          | 262 +++++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   8 +
 src/test/regress/expected/rowsecurity.out | 271 ++++++++++++++++++++++
 src/test/regress/sql/rowsecurity.sql      | 126 ++++++++++
 5 files changed, 687 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 8ffa7e83275..1cf498cb454 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3947,6 +3947,26 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
         <literal>TABLESPACE</literal>.
        </para></entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>,
+        <parameter>policy_name</parameter> <type>name</type>
+        <optional>, <literal>VARIADIC</literal> <parameter>options</parameter>
+        <type>text</type> </optional> )
+        <returnvalue>setof text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement for the
+        named row-level security policy on the specified table.  The result
+        is returned as a single row.
+        The following option is supported: <literal>pretty</literal>
+        (boolean) for formatted output.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c
index f32fcd453ef..728d3648979 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_tablespace.h"
 #include "commands/tablespace.h"
 #include "common/relpath.h"
@@ -86,6 +87,9 @@ static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner
 static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull);
 static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
 										  bool no_owner, bool no_tablespace);
+static List *pg_get_policy_ddl_internal(Oid tableID, const char *policyName,
+										bool pretty);
+static const char *get_policy_cmd_name(char cmd);
 
 
 /*
@@ -1185,3 +1189,261 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
 		SRF_RETURN_DONE(funcctx);
 	}
 }
+
+/*
+ * get_policy_cmd_name
+ *		Map a pg_policy.polcmd char to its SQL keyword.
+ */
+static const char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command: %d", (int) cmd);
+	}
+}
+
+/*
+ * pg_get_policy_ddl_internal
+ *		Generate the DDL statement to recreate a row-level security policy.
+ *
+ * Returns a List containing a single palloc'd string with the CREATE POLICY
+ * statement.  Returning a List keeps the calling convention consistent with
+ * the rest of the pg_get_*_ddl family even though only one row is produced.
+ */
+static List *
+pg_get_policy_ddl_internal(Oid tableID, const char *policyName, bool pretty)
+{
+	Relation	pgPolicyRel;
+	HeapTuple	tuplePolicy;
+	Form_pg_policy policyForm;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	StringInfoData buf;
+	Datum		valueDatum;
+	bool		attrIsNull;
+	char	   *targetTable;
+	List	   *statements = NIL;
+
+	/* Validate that the relation exists */
+	{
+		char	   *relname = get_rel_name(tableID);
+		char	   *nspname;
+
+		if (relname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_TABLE),
+					 errmsg("relation with OID %u does not exist", tableID)));
+
+		nspname = get_namespace_name(get_rel_namespace(tableID));
+		if (nspname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_SCHEMA),
+					 errmsg("schema for relation with OID %u does not exist",
+							tableID)));
+
+		targetTable = quote_qualified_identifier(nspname, relname);
+		pfree(relname);
+		pfree(nspname);
+	}
+
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(policyName));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						policyName, targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	initStringInfo(&buf);
+
+	/* Build the CREATE POLICY statement */
+	appendStringInfo(&buf, "CREATE POLICY %s ON %s",
+					 quote_identifier(policyName),
+					 targetTable);
+
+	/*
+	 * Emit AS RESTRICTIVE only when it differs from the default (PERMISSIVE).
+	 */
+	if (!policyForm->polpermissive)
+		append_ddl_option(&buf, pretty, 4, "AS RESTRICTIVE");
+
+	/*
+	 * Emit FOR <cmd> only when it differs from the default (ALL, encoded as
+	 * '*').
+	 */
+	if (policyForm->polcmd != '*')
+		append_ddl_option(&buf, pretty, 4, "FOR %s",
+						  get_policy_cmd_name(policyForm->polcmd));
+
+	/*
+	 * Emit TO <roles> only when it differs from the default (PUBLIC).  PUBLIC
+	 * is encoded in polroles as a single InvalidOid element, so we omit the
+	 * clause whenever every entry is InvalidOid.
+	 */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (role_names.len > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, quote_identifier(rolename));
+			}
+		}
+
+		if (role_names.len > 0)
+			append_ddl_option(&buf, pretty, 4, "TO %s", role_names.data);
+
+		pfree(role_names.data);
+		pfree(policy_roles);
+	}
+
+	/* USING expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "USING (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	/* WITH CHECK expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "WITH CHECK (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	appendStringInfoChar(&buf, ';');
+
+	statements = lappend(statements, pstrdup(buf.data));
+
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+	pfree(buf.data);
+
+	return statements;
+}
+
+/*
+ * pg_get_policy_ddl
+ *		Return DDL to recreate a row-level security policy as a single text row.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	List	   *statements;
+
+	if (SRF_IS_FIRSTCALL())
+	{
+		MemoryContext oldcontext;
+		Oid			tableID;
+		Name		policyName;
+		DdlOption	opts[] = {
+			{"pretty", DDL_OPT_BOOL},
+		};
+
+		funcctx = SRF_FIRSTCALL_INIT();
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+		{
+			MemoryContextSwitchTo(oldcontext);
+			SRF_RETURN_DONE(funcctx);
+		}
+
+		tableID = PG_GETARG_OID(0);
+		policyName = PG_GETARG_NAME(1);
+
+		parse_ddl_options(fcinfo, 2, opts, lengthof(opts));
+
+		statements = pg_get_policy_ddl_internal(tableID,
+												NameStr(*policyName),
+												opts[0].isset && opts[0].boolval);
+		funcctx->user_fctx = statements;
+		funcctx->max_calls = list_length(statements);
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	funcctx = SRF_PERCALL_SETUP();
+	statements = (List *) funcctx->user_fctx;
+
+	if (funcctx->call_cntr < funcctx->max_calls)
+	{
+		char	   *stmt;
+
+		stmt = list_nth(statements, funcctx->call_cntr);
+
+		SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+	}
+	else
+	{
+		list_free_deep(statements);
+		SRF_RETURN_DONE(funcctx);
+	}
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..1a3241300ca 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,14 @@
   proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
   proargmodes => '{i,v}', proargdefaults => '{NULL}',
   prosrc => 'pg_get_database_ddl' },
+{ oid => '6517', descr => 'get DDL to recreate a row-level security policy',
+  proname => 'pg_get_policy_ddl', prorows => '1', provariadic => 'text',
+  proisstrict => 'f', proretset => 't', provolatile => 's',
+  pronargdefaults => '1', prorettype => 'text',
+  proargtypes => 'regclass name _text',
+  proallargtypes => '{regclass,name,_text}',
+  proargmodes => '{i,i,v}', proargdefaults => '{NULL}',
+  prosrc => 'pg_get_policy_ddl' },
 { oid => '2509',
   descr => 'deparse an encoded expression with pretty-print option',
   proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 3a5e82c35bd..c7de6e47c1d 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -5195,6 +5195,277 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(table, policy_name, VARIADIC options) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+ count 
+-------
+     0
+(1 row)
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+ERROR:  relation "nonexistent_tbl" does not exist
+LINE 1: SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1')...
+                                        ^
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+ERROR:  policy "nonexistent_pol" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Invalid option name
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'badopt', 'true');
+ERROR:  unrecognized option: "badopt"
+-- Invalid boolean value for option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'maybe');
+ERROR:  invalid value for boolean option "pretty": maybe
+-- Duplicate option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true', 'pretty', 'false');
+ERROR:  option "pretty" is specified more than once
+-- Without pretty formatting (default)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                              +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+                                   pg_get_policy_ddl                                    
+----------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 FOR SELECT USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+                                       pg_get_policy_ddl                                       
+-----------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 FOR INSERT WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'false');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'false');
+                                 pg_get_policy_ddl                                  
+------------------------------------------------------------------------------------
+ CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 FOR DELETE USING ((cid < 8));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'false');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'false');
+                                                      pg_get_policy_ddl                                                      
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 TO regress_rls_exempt_user WITH CHECK ((cid = ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2)));
+(1 row)
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+    AS RESTRICTIVE
+    USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+    FOR SELECT
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+    FOR INSERT
+    WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+    FOR UPDATE
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+    FOR DELETE
+    USING (cid < 8);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_dave, regress_rls_alice
+    USING (true);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_exempt_user
+    WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+\pset format aligned
+-- Pass options via VARIADIC ARRAY[...] syntax
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty','false']);
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                              +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3',
+                                VARIADIC ARRAY['pretty','false']);
+                                   pg_get_policy_ddl                                    
+----------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty','true']);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3',
+                                VARIADIC ARRAY['pretty','true']);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+\pset format aligned
+-- Empty VARIADIC array behaves as default (no options)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY[]::text[]);
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                              +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+-- Unrecognized option via VARIADIC ARRAY
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['badopt','true']);
+ERROR:  unrecognized option: "badopt"
+-- Invalid boolean value via VARIADIC ARRAY
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty','maybe']);
+ERROR:  invalid value for boolean option "pretty": maybe
+-- Odd number of elements via VARIADIC ARRAY
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty']);
+ERROR:  variadic arguments must be name/value pairs
+HINT:  Provide an even number of variadic arguments that can be divided into pairs.
+-- Round-trip: the generated DDL must be re-executable, including for atomic
+-- boolean expressions that pg_get_expr() does not parenthesize.
+CREATE TABLE rls_rt (a int);
+CREATE POLICY rt_true ON rls_rt USING (true);
+CREATE POLICY rt_false ON rls_rt FOR INSERT WITH CHECK (false);
+CREATE TEMP TABLE rt_ddl AS
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_true') AS ddl
+    UNION ALL
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_false');
+DROP POLICY rt_true ON rls_rt;
+DROP POLICY rt_false ON rls_rt;
+SELECT ddl FROM rt_ddl ORDER BY ddl \gexec
+CREATE POLICY rt_false ON regress_rls_schema.rls_rt FOR INSERT WITH CHECK (false);
+CREATE POLICY rt_true ON regress_rls_schema.rls_rt USING (true);
+SELECT polname FROM pg_policy WHERE polrelid = 'rls_rt'::regclass ORDER BY polname;
+ polname  
+----------
+ rt_false
+ rt_true
+(2 rows)
+
+DROP TABLE rls_rt;
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 6b3566271df..2e8f11e63a2 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2598,6 +2598,132 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(table, policy_name, VARIADIC options) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+
+-- Invalid option name
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'badopt', 'true');
+-- Invalid boolean value for option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'maybe');
+-- Duplicate option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true', 'pretty', 'false');
+
+-- Without pretty formatting (default)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'false');
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'true');
+\pset format aligned
+
+-- Pass options via VARIADIC ARRAY[...] syntax
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty','false']);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3',
+                                VARIADIC ARRAY['pretty','false']);
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty','true']);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3',
+                                VARIADIC ARRAY['pretty','true']);
+\pset format aligned
+-- Empty VARIADIC array behaves as default (no options)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY[]::text[]);
+-- Unrecognized option via VARIADIC ARRAY
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['badopt','true']);
+-- Invalid boolean value via VARIADIC ARRAY
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty','maybe']);
+-- Odd number of elements via VARIADIC ARRAY
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty']);
+
+-- Round-trip: the generated DDL must be re-executable, including for atomic
+-- boolean expressions that pg_get_expr() does not parenthesize.
+CREATE TABLE rls_rt (a int);
+CREATE POLICY rt_true ON rls_rt USING (true);
+CREATE POLICY rt_false ON rls_rt FOR INSERT WITH CHECK (false);
+CREATE TEMP TABLE rt_ddl AS
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_true') AS ddl
+    UNION ALL
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_false');
+DROP POLICY rt_true ON rls_rt;
+DROP POLICY rt_false ON rls_rt;
+SELECT ddl FROM rt_ddl ORDER BY ddl \gexec
+SELECT polname FROM pg_policy WHERE polrelid = 'rls_rt'::regclass ORDER BY polname;
+DROP TABLE rls_rt;
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-06-29 13:42  Akshay Joshi <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2026-06-29 13:42 UTC (permalink / raw)
  To: solai v <[email protected]>; +Cc: Ilmar Y <[email protected]>; [email protected]

Rebased *v13* patch is now ready for review and commit.

On Mon, Jun 22, 2026 at 6:49 PM Akshay Joshi <[email protected]>
wrote:

> *Chao Li* found an issue in my other patch for *pg_get_table_ddl.* Specifically,
> the existing pattern in pg_proc.dat for variadic-text functions (e.g.,
> jsonb_delete, json_extract_path) uses *_text* instead of *text* at the
> variadic position in both proargtypes and proallargtypes, with provariadic
> => 'text'. This is the convention documented by the sanity check in
> src/test/regress/sql/opr_sanity.sql.
>
> I realized the same issue was present in this patch as well, so I have
> fixed it and added corresponding test cases.
>
> The v12 patch is now ready for review and commit.
>
> On Mon, Jun 22, 2026 at 6:34 PM Akshay Joshi <
> [email protected]> wrote:
>
>>
>>
>> On Mon, Jun 8, 2026 at 6:10 PM solai v <[email protected]> wrote:
>>
>>> Hi all,
>>>
>>>
>>> On Mon, Jun 8, 2026 at 5:32 PM Japin Li <[email protected]> wrote:
>>> >
>>> > On Fri, 29 May 2026 at 12:20, Akshay Joshi <
>>> [email protected]> wrote:
>>> > > Thanks for the reviews.
>>> > >
>>> > > My original patch (v9) was actually correct. After considering
>>> Japin's review comment, I initially thought the extra
>>> > > parentheses weren't necessary, but they are indeed required for
>>> handling boolean values properly in non-pretty mode too,
>>> > > so I kept them in USING (%s) / WITH CHECK (%s) for both modes.
>>> > >
>>> >
>>> > My bad!  I had not considered this situation.
>>> >
>>> > > `pg_get_expr()` only adds outer parentheses for composite
>>> expressions (via the deparsers for `OpExpr`, `BoolExpr`, etc.).
>>> > > For atomic top-level nodes like `Const`, `Var`, `current_user`,
>>> `NULL`, etc.
>>> > > For example:
>>> > >
>>> > >     CREATE POLICY p ON t USING (true);
>>> > >     SELECT pg_get_policy_ddl('t', 'p');  -- previously: ... USING
>>> true;  (syntax error)
>>> > >
>>> > > This is exactly why `pg_dump` always wraps the expression
>>> unconditionally; see `src/bin/pg_dump/pg_dump.c`:4473-4477:
>>> > >
>>> > >     if (polinfo->polqual != NULL)
>>> > >         appendPQExpBuffer(query, " USING (%s)", polinfo->polqual);
>>> > >     if (polinfo->polwithcheck != NULL)
>>> > >         appendPQExpBuffer(query, " WITH CHECK (%s)",
>>> polinfo->polwithcheck);
>>> > >
>>> > > I've also added a round-trip regression test with `USING (true)` /
>>> `WITH CHECK (false)` that captures the generated DDL,
>>> > > drops the policies, re-executes the DDL, and verifies the policies
>>> are recreated.
>>> > >
>>> > > v11 Patch attached for review.
>>> > >
>>> > > On Thu, May 28, 2026 at 7:12 PM Ilmar Y <[email protected]> wrote:
>>> > >
>>> > >  The following review has been posted through the commitfest
>>> application:
>>> > >  make installcheck-world:  not tested
>>> > >  Implements feature:       tested, failed
>>> > >  Spec compliant:           not tested
>>> > >  Documentation:            not tested
>>> > >
>>> > >  Hi,
>>> > >
>>> > >  I looked at v10, focused on whether the generated CREATE POLICY
>>> statement
>>> > >  can be executed again.
>>> > >
>>> > >  The patch applies cleanly on current master at
>>> > >  8a86aa313a714adc56c74e4b08793e4e6102b5ca.
>>> > >
>>> > >  git diff --check reports no issues.
>>> > >
>>> > >  I built with:
>>> > >
>>> > >  ./configure --prefix="$PWD/pg-install" --without-readline
>>> --without-zlib --without-icu
>>> > >  make -s -j8
>>> > >  make -s install
>>> > >
>>> > >  make -C src/test/regress check TESTS=rowsecurity
>>> > >
>>> > >  ended up running the full parallel_schedule in this makefile; all
>>> 245 tests
>>> > >  passed, including rowsecurity.
>>> > >
>>> > >  I found one correctness issue in the generated non-pretty DDL.  The
>>> code
>>> > >  assumes that pg_get_expr_ext(..., false) already returns the
>>> parentheses
>>> > >  required by CREATE POLICY syntax, but that is not true for simple
>>> boolean
>>> > >  constants.
>>> > >
>>> > >  For example:
>>> > >
>>> > >  CREATE TABLE t(a int);
>>> > >  CREATE POLICY p_true ON t USING (true);
>>> > >  SELECT ddl FROM pg_get_policy_ddl('t', 'p_true', 'pretty', 'false')
>>> AS ddl;
>>> > >
>>> > >  returns:
>>> > >
>>> > >  CREATE POLICY p_true ON public.t USING true;
>>> > >
>>> > >  If I drop the policy and execute that generated statement, it fails:
>>> > >
>>> > >  ERROR:  syntax error at or near "true"
>>> > >  LINE 1: CREATE POLICY p_true ON public.t USING true;
>>> > >                                                 ^
>>> > >
>>> > >  The same issue reproduces for WITH CHECK:
>>> > >
>>> > >  CREATE POLICY p_check ON t FOR INSERT WITH CHECK (false);
>>> > >
>>> > >  is reconstructed as:
>>> > >
>>> > >  CREATE POLICY p_check ON public.t FOR INSERT WITH CHECK false;
>>> > >
>>> > >  and executing it fails at "false".
>>> > >
>>> > >  So I think USING and WITH CHECK need to be parenthesized in
>>> non-pretty mode
>>> > >  too, or the tests should include a round-trip execution check for
>>> generated
>>> > >  DDL with simple boolean expressions.
>>> > >
>>> > >  I used two small SQL reproducers for the manual checks; the
>>> complete repro is
>>> > >  included above.
>>> > >
>>> > >  I have not reviewed the broader pg_get_*_ddl API design or every
>>> possible
>>> > >  policy expression form.
>>> > >
>>> > >  Regards,
>>> > >  Ilmar Yunusov
>>> > >
>>> > >  The new status of this patch is: Waiting on Author
>>> >
>>>
>>>
>>> I reviewed and tested the V11 pg_get_policy_ddl() patch. I verified
>>> the basic functionality by creating various row-level security
>>> policies and checking the reconstructed DDL returned by
>>> pg_get_policy_ddl(). The function correctly reconstructed policies for
>>> different command types (SELECT, INSERT, UPDATE, DELETE), PERMISSIVE
>>> and RESTRICTIVE policies, multiple role lists, quoted identifiers,
>>> USING clauses, and WITH CHECK clauses.
>>> I also tested more complex cases involving subqueries. Policies
>>> containing EXISTS subqueries in both USING and WITH CHECK clauses were
>>> reconstructed successfully. Nested subqueries were also handled
>>> correctly, and I did not encounter any issues related to expression
>>> reconstruction or variable handling. I verified NULL input handling as
>>> well. Passing NULL values for the table name or policy name returned
>>> no rows as expected. Invalid relation names resulted in the expected
>>> regclass resolution error. One observation from testing is that the
>>> generated DDL omits default clauses such as FOR ALL and TO PUBLIC. For
>>> example, a policy created with FOR ALL TO PUBLIC is reconstructed
>>> without those clauses, while still producing semantically equivalent
>>> DDL. I'm not sure whether this is intentional or if the function is
>>> expected to reproduce all clauses explicitly, but it may be worth
>>> discussing.
>>> Overall, the patch worked as expected in all scenarios I tested, and I
>>> did not find any functional issues.
>>>
>>
>> Thanks for the review. The omission of FOR ALL and TO PUBLIC is
>> intentional and matches the long-standing convention used by
>> pg_get_indexdef, pg_get_constraintdef, pg_get_viewdef, etc.: emit a clause
>> only when it differs from the parser's default. The result is semantically
>> equivalent to the CREATE POLICY DDL.
>>
>>>
>>>
>>> Regards,
>>> Solai
>>>
>>


Attachments:

  [application/octet-stream] v13-0001-Add-pg_get_policy_ddl-to-reconstruct-CREATE.patch (31.1K, ../../CANxoLDeqR6wVCoXjsEWv4M_mhng-GCtt1KxFdfM-8PKwushUCA@mail.gmail.com/3-v13-0001-Add-pg_get_policy_ddl-to-reconstruct-CREATE.patch)
  download | inline diff:
From 2bc84100707c13e09b2122ae0dae531bc3487982 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 22 May 2026 18:18:07 +0530
Subject: [PATCH v13] Add pg_get_policy_ddl() to reconstruct CREATE POLICY
 statements

This patch introduces a new system function:

  pg_get_policy_ddl(table regclass,
                    policy_name name,
                    VARIADIC options text[])
  RETURNS SETOF text

which reconstructs the CREATE POLICY statement for the named row-level
security policy on the specified table.  Although a single statement is
produced, the function returns a SETOF text result so its calling
convention matches the rest of the pg_get_*_ddl family.

The reconstructed DDL includes the policy's permissiveness (AS
RESTRICTIVE), command (FOR SELECT/INSERT/UPDATE/DELETE), role list (TO
<roles>), USING qualification, and WITH CHECK expression.  Clauses
whose value equals the parser's default -- PERMISSIVE, FOR ALL, and
TO PUBLIC -- are omitted from the output, matching the convention used
by pg_get_indexdef, pg_get_constraintdef, pg_get_viewdef, and similar
functions.  The result is therefore semantically equivalent to the
original DDL, not lexically identical.

Options are passed as alternating name/value text pairs via the
VARIADIC argument; the supported option is:

  pretty (boolean) -- format the output for readability.

NULL inputs for the table or policy name yield no rows.  An invalid
relation name surfaces the regclass resolution error; a non-existent
policy raises an explicit "policy ... does not exist" error.  Option
parsing rejects unrecognized names, invalid boolean values, duplicate
options, and an odd number of variadic arguments.

Usage examples:

  -- compact form (default)
  SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1');
  SELECT * FROM pg_get_policy_ddl(16564, 'pol1');

  -- pretty-printed form
  SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1',
                                  'pretty', 'true');

  -- options supplied as a constructed array
  SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1',
                                  VARIADIC ARRAY['pretty','true']);

Regression coverage is added to src/test/regress/sql/rowsecurity.sql
and exercises PERMISSIVE/RESTRICTIVE policies, all FOR command
variants, multi-role TO lists, subquery expressions in USING and
WITH CHECK, pretty and non-pretty output, NULL and error paths, the
VARIADIC ARRAY[...] call syntax, and a round-trip test that drops
and re-executes the generated DDL.

Author: Akshay Joshi <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  20 ++
 src/backend/utils/adt/ddlutils.c          | 262 +++++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   8 +
 src/test/regress/expected/rowsecurity.out | 271 ++++++++++++++++++++++
 src/test/regress/sql/rowsecurity.sql      | 126 ++++++++++
 5 files changed, 687 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 69ef3857cfa..eda94d8be58 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3960,6 +3960,26 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
         is false, the <literal>OWNER</literal> clause is omitted.
        </para></entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>,
+        <parameter>policy_name</parameter> <type>name</type>
+        <optional>, <literal>VARIADIC</literal> <parameter>options</parameter>
+        <type>text</type> </optional> )
+        <returnvalue>setof text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement for the
+        named row-level security policy on the specified table.  The result
+        is returned as a single row.
+        The following option is supported: <literal>pretty</literal>
+        (boolean) for formatted output.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c
index a70f1c28655..6732226c6bd 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_tablespace.h"
 #include "commands/tablespace.h"
 #include "common/relpath.h"
@@ -56,6 +57,9 @@ static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner
 static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid);
 static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
 										  bool no_owner, bool no_tablespace);
+static List *pg_get_policy_ddl_internal(Oid tableID, const char *policyName,
+										bool pretty);
+static const char *get_policy_cmd_name(char cmd);
 
 
 /*
@@ -975,3 +979,261 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
 		SRF_RETURN_DONE(funcctx);
 	}
 }
+
+/*
+ * get_policy_cmd_name
+ *		Map a pg_policy.polcmd char to its SQL keyword.
+ */
+static const char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case '*':
+			return "ALL";
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command: %d", (int) cmd);
+	}
+}
+
+/*
+ * pg_get_policy_ddl_internal
+ *		Generate the DDL statement to recreate a row-level security policy.
+ *
+ * Returns a List containing a single palloc'd string with the CREATE POLICY
+ * statement.  Returning a List keeps the calling convention consistent with
+ * the rest of the pg_get_*_ddl family even though only one row is produced.
+ */
+static List *
+pg_get_policy_ddl_internal(Oid tableID, const char *policyName, bool pretty)
+{
+	Relation	pgPolicyRel;
+	HeapTuple	tuplePolicy;
+	Form_pg_policy policyForm;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	StringInfoData buf;
+	Datum		valueDatum;
+	bool		attrIsNull;
+	char	   *targetTable;
+	List	   *statements = NIL;
+
+	/* Validate that the relation exists */
+	{
+		char	   *relname = get_rel_name(tableID);
+		char	   *nspname;
+
+		if (relname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_TABLE),
+					 errmsg("relation with OID %u does not exist", tableID)));
+
+		nspname = get_namespace_name(get_rel_namespace(tableID));
+		if (nspname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_SCHEMA),
+					 errmsg("schema for relation with OID %u does not exist",
+							tableID)));
+
+		targetTable = quote_qualified_identifier(nspname, relname);
+		pfree(relname);
+		pfree(nspname);
+	}
+
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(policyName));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						policyName, targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	initStringInfo(&buf);
+
+	/* Build the CREATE POLICY statement */
+	appendStringInfo(&buf, "CREATE POLICY %s ON %s",
+					 quote_identifier(policyName),
+					 targetTable);
+
+	/*
+	 * Emit AS RESTRICTIVE only when it differs from the default (PERMISSIVE).
+	 */
+	if (!policyForm->polpermissive)
+		append_ddl_option(&buf, pretty, 4, "AS RESTRICTIVE");
+
+	/*
+	 * Emit FOR <cmd> only when it differs from the default (ALL, encoded as
+	 * '*').
+	 */
+	if (policyForm->polcmd != '*')
+		append_ddl_option(&buf, pretty, 4, "FOR %s",
+						  get_policy_cmd_name(policyForm->polcmd));
+
+	/*
+	 * Emit TO <roles> only when it differs from the default (PUBLIC).  PUBLIC
+	 * is encoded in polroles as a single InvalidOid element, so we omit the
+	 * clause whenever every entry is InvalidOid.
+	 */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypePCopy(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (role_names.len > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, quote_identifier(rolename));
+			}
+		}
+
+		if (role_names.len > 0)
+			append_ddl_option(&buf, pretty, 4, "TO %s", role_names.data);
+
+		pfree(role_names.data);
+		pfree(policy_roles);
+	}
+
+	/* USING expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "USING (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	/* WITH CHECK expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "WITH CHECK (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	appendStringInfoChar(&buf, ';');
+
+	statements = lappend(statements, pstrdup(buf.data));
+
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+	pfree(buf.data);
+
+	return statements;
+}
+
+/*
+ * pg_get_policy_ddl
+ *		Return DDL to recreate a row-level security policy as a single text row.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	List	   *statements;
+
+	if (SRF_IS_FIRSTCALL())
+	{
+		MemoryContext oldcontext;
+		Oid			tableID;
+		Name		policyName;
+		DdlOption	opts[] = {
+			{"pretty", DDL_OPT_BOOL},
+		};
+
+		funcctx = SRF_FIRSTCALL_INIT();
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+		{
+			MemoryContextSwitchTo(oldcontext);
+			SRF_RETURN_DONE(funcctx);
+		}
+
+		tableID = PG_GETARG_OID(0);
+		policyName = PG_GETARG_NAME(1);
+
+		parse_ddl_options(fcinfo, 2, opts, lengthof(opts));
+
+		statements = pg_get_policy_ddl_internal(tableID,
+												NameStr(*policyName),
+												opts[0].isset && opts[0].boolval);
+		funcctx->user_fctx = statements;
+		funcctx->max_calls = list_length(statements);
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	funcctx = SRF_PERCALL_SETUP();
+	statements = (List *) funcctx->user_fctx;
+
+	if (funcctx->call_cntr < funcctx->max_calls)
+	{
+		char	   *stmt;
+
+		stmt = list_nth(statements, funcctx->call_cntr);
+
+		SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+	}
+	else
+	{
+		list_free_deep(statements);
+		SRF_RETURN_DONE(funcctx);
+	}
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 402d869710b..80b03bc9085 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8614,6 +8614,14 @@
   prorettype => 'text', proargtypes => 'regdatabase bool bool bool',
   proargnames => '{database,pretty,owner,tablespace}',
   proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' },
+{ oid => '6517', descr => 'get DDL to recreate a row-level security policy',
+  proname => 'pg_get_policy_ddl', prorows => '1', provariadic => 'text',
+  proisstrict => 'f', proretset => 't', provolatile => 's',
+  pronargdefaults => '1', prorettype => 'text',
+  proargtypes => 'regclass name _text',
+  proallargtypes => '{regclass,name,_text}',
+  proargmodes => '{i,i,v}', proargdefaults => '{NULL}',
+  prosrc => 'pg_get_policy_ddl' },
 { oid => '2509',
   descr => 'deparse an encoded expression with pretty-print option',
   proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 3a5e82c35bd..c7de6e47c1d 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -5195,6 +5195,277 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(table, policy_name, VARIADIC options) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+ count 
+-------
+     0
+(1 row)
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+ERROR:  relation "nonexistent_tbl" does not exist
+LINE 1: SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1')...
+                                        ^
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+ERROR:  policy "nonexistent_pol" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Invalid option name
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'badopt', 'true');
+ERROR:  unrecognized option: "badopt"
+-- Invalid boolean value for option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'maybe');
+ERROR:  invalid value for boolean option "pretty": maybe
+-- Duplicate option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true', 'pretty', 'false');
+ERROR:  option "pretty" is specified more than once
+-- Without pretty formatting (default)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                              +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+                                   pg_get_policy_ddl                                    
+----------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 FOR SELECT USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+                                       pg_get_policy_ddl                                       
+-----------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 FOR INSERT WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'false');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'false');
+                                 pg_get_policy_ddl                                  
+------------------------------------------------------------------------------------
+ CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 FOR DELETE USING ((cid < 8));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'false');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'false');
+                                                      pg_get_policy_ddl                                                      
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 TO regress_rls_exempt_user WITH CHECK ((cid = ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2)));
+(1 row)
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+    AS RESTRICTIVE
+    USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+    FOR SELECT
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+    FOR INSERT
+    WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+    FOR UPDATE
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+    FOR DELETE
+    USING (cid < 8);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_dave, regress_rls_alice
+    USING (true);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'true');
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_exempt_user
+    WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+\pset format aligned
+-- Pass options via VARIADIC ARRAY[...] syntax
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty','false']);
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                              +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3',
+                                VARIADIC ARRAY['pretty','false']);
+                                   pg_get_policy_ddl                                    
+----------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty','true']);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3',
+                                VARIADIC ARRAY['pretty','true']);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+\pset format aligned
+-- Empty VARIADIC array behaves as default (no options)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY[]::text[]);
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                              +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+-- Unrecognized option via VARIADIC ARRAY
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['badopt','true']);
+ERROR:  unrecognized option: "badopt"
+-- Invalid boolean value via VARIADIC ARRAY
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty','maybe']);
+ERROR:  invalid value for boolean option "pretty": maybe
+-- Odd number of elements via VARIADIC ARRAY
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty']);
+ERROR:  variadic arguments must be name/value pairs
+HINT:  Provide an even number of variadic arguments that can be divided into pairs.
+-- Round-trip: the generated DDL must be re-executable, including for atomic
+-- boolean expressions that pg_get_expr() does not parenthesize.
+CREATE TABLE rls_rt (a int);
+CREATE POLICY rt_true ON rls_rt USING (true);
+CREATE POLICY rt_false ON rls_rt FOR INSERT WITH CHECK (false);
+CREATE TEMP TABLE rt_ddl AS
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_true') AS ddl
+    UNION ALL
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_false');
+DROP POLICY rt_true ON rls_rt;
+DROP POLICY rt_false ON rls_rt;
+SELECT ddl FROM rt_ddl ORDER BY ddl \gexec
+CREATE POLICY rt_false ON regress_rls_schema.rls_rt FOR INSERT WITH CHECK (false);
+CREATE POLICY rt_true ON regress_rls_schema.rls_rt USING (true);
+SELECT polname FROM pg_policy WHERE polrelid = 'rls_rt'::regclass ORDER BY polname;
+ polname  
+----------
+ rt_false
+ rt_true
+(2 rows)
+
+DROP TABLE rls_rt;
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 6b3566271df..2e8f11e63a2 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2598,6 +2598,132 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(table, policy_name, VARIADIC options) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+
+-- Invalid option name
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'badopt', 'true');
+-- Invalid boolean value for option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'maybe');
+-- Duplicate option
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true', 'pretty', 'false');
+
+-- Without pretty formatting (default)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'false');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'false');
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', 'pretty', 'true');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', 'pretty', 'true');
+\pset format aligned
+
+-- Pass options via VARIADIC ARRAY[...] syntax
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty','false']);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3',
+                                VARIADIC ARRAY['pretty','false']);
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty','true']);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3',
+                                VARIADIC ARRAY['pretty','true']);
+\pset format aligned
+-- Empty VARIADIC array behaves as default (no options)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY[]::text[]);
+-- Unrecognized option via VARIADIC ARRAY
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['badopt','true']);
+-- Invalid boolean value via VARIADIC ARRAY
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty','maybe']);
+-- Odd number of elements via VARIADIC ARRAY
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1',
+                                VARIADIC ARRAY['pretty']);
+
+-- Round-trip: the generated DDL must be re-executable, including for atomic
+-- boolean expressions that pg_get_expr() does not parenthesize.
+CREATE TABLE rls_rt (a int);
+CREATE POLICY rt_true ON rls_rt USING (true);
+CREATE POLICY rt_false ON rls_rt FOR INSERT WITH CHECK (false);
+CREATE TEMP TABLE rt_ddl AS
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_true') AS ddl
+    UNION ALL
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_false');
+DROP POLICY rt_true ON rls_rt;
+DROP POLICY rt_false ON rls_rt;
+SELECT ddl FROM rt_ddl ORDER BY ddl \gexec
+SELECT polname FROM pg_policy WHERE polrelid = 'rls_rt'::regclass ORDER BY polname;
+DROP TABLE rls_rt;
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-06-30 11:18  Akshay Joshi <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2026-06-30 11:18 UTC (permalink / raw)
  To: solai v <[email protected]>; +Cc: Ilmar Y <[email protected]>; [email protected]

All,

I've updated the patch to align with the interface change introduced by
commit *d6ed87d1989* (Andrew Dunstan, 2026-06-26 "Use named boolean
parameters for pg_get_*_ddl option arguments").

That commit replaced the VARIADIC text[] alternating key/value option
interface with typed named boolean parameters across pg_get_role_ddl(),
pg_get_tablespace_ddl(), and pg_get_database_ddl(), removing the
DdlOption/parse_ddl_options() machinery in favour of direct
PG_GETARG_BOOL() calls.

Updated patch v14 is ready for review/commit.

On Mon, Jun 29, 2026 at 7:12 PM Akshay Joshi <[email protected]>
wrote:

> Rebased *v13* patch is now ready for review and commit.
>
> On Mon, Jun 22, 2026 at 6:49 PM Akshay Joshi <
> [email protected]> wrote:
>
>> *Chao Li* found an issue in my other patch for *pg_get_table_ddl.* Specifically,
>> the existing pattern in pg_proc.dat for variadic-text functions (e.g.,
>> jsonb_delete, json_extract_path) uses *_text* instead of *text* at the
>> variadic position in both proargtypes and proallargtypes, with provariadic
>> => 'text'. This is the convention documented by the sanity check in
>> src/test/regress/sql/opr_sanity.sql.
>>
>> I realized the same issue was present in this patch as well, so I have
>> fixed it and added corresponding test cases.
>>
>> The v12 patch is now ready for review and commit.
>>
>> On Mon, Jun 22, 2026 at 6:34 PM Akshay Joshi <
>> [email protected]> wrote:
>>
>>>
>>>
>>> On Mon, Jun 8, 2026 at 6:10 PM solai v <[email protected]> wrote:
>>>
>>>> Hi all,
>>>>
>>>>
>>>> On Mon, Jun 8, 2026 at 5:32 PM Japin Li <[email protected]> wrote:
>>>> >
>>>> > On Fri, 29 May 2026 at 12:20, Akshay Joshi <
>>>> [email protected]> wrote:
>>>> > > Thanks for the reviews.
>>>> > >
>>>> > > My original patch (v9) was actually correct. After considering
>>>> Japin's review comment, I initially thought the extra
>>>> > > parentheses weren't necessary, but they are indeed required for
>>>> handling boolean values properly in non-pretty mode too,
>>>> > > so I kept them in USING (%s) / WITH CHECK (%s) for both modes.
>>>> > >
>>>> >
>>>> > My bad!  I had not considered this situation.
>>>> >
>>>> > > `pg_get_expr()` only adds outer parentheses for composite
>>>> expressions (via the deparsers for `OpExpr`, `BoolExpr`, etc.).
>>>> > > For atomic top-level nodes like `Const`, `Var`, `current_user`,
>>>> `NULL`, etc.
>>>> > > For example:
>>>> > >
>>>> > >     CREATE POLICY p ON t USING (true);
>>>> > >     SELECT pg_get_policy_ddl('t', 'p');  -- previously: ... USING
>>>> true;  (syntax error)
>>>> > >
>>>> > > This is exactly why `pg_dump` always wraps the expression
>>>> unconditionally; see `src/bin/pg_dump/pg_dump.c`:4473-4477:
>>>> > >
>>>> > >     if (polinfo->polqual != NULL)
>>>> > >         appendPQExpBuffer(query, " USING (%s)", polinfo->polqual);
>>>> > >     if (polinfo->polwithcheck != NULL)
>>>> > >         appendPQExpBuffer(query, " WITH CHECK (%s)",
>>>> polinfo->polwithcheck);
>>>> > >
>>>> > > I've also added a round-trip regression test with `USING (true)` /
>>>> `WITH CHECK (false)` that captures the generated DDL,
>>>> > > drops the policies, re-executes the DDL, and verifies the policies
>>>> are recreated.
>>>> > >
>>>> > > v11 Patch attached for review.
>>>> > >
>>>> > > On Thu, May 28, 2026 at 7:12 PM Ilmar Y <[email protected]>
>>>> wrote:
>>>> > >
>>>> > >  The following review has been posted through the commitfest
>>>> application:
>>>> > >  make installcheck-world:  not tested
>>>> > >  Implements feature:       tested, failed
>>>> > >  Spec compliant:           not tested
>>>> > >  Documentation:            not tested
>>>> > >
>>>> > >  Hi,
>>>> > >
>>>> > >  I looked at v10, focused on whether the generated CREATE POLICY
>>>> statement
>>>> > >  can be executed again.
>>>> > >
>>>> > >  The patch applies cleanly on current master at
>>>> > >  8a86aa313a714adc56c74e4b08793e4e6102b5ca.
>>>> > >
>>>> > >  git diff --check reports no issues.
>>>> > >
>>>> > >  I built with:
>>>> > >
>>>> > >  ./configure --prefix="$PWD/pg-install" --without-readline
>>>> --without-zlib --without-icu
>>>> > >  make -s -j8
>>>> > >  make -s install
>>>> > >
>>>> > >  make -C src/test/regress check TESTS=rowsecurity
>>>> > >
>>>> > >  ended up running the full parallel_schedule in this makefile; all
>>>> 245 tests
>>>> > >  passed, including rowsecurity.
>>>> > >
>>>> > >  I found one correctness issue in the generated non-pretty DDL.
>>>> The code
>>>> > >  assumes that pg_get_expr_ext(..., false) already returns the
>>>> parentheses
>>>> > >  required by CREATE POLICY syntax, but that is not true for simple
>>>> boolean
>>>> > >  constants.
>>>> > >
>>>> > >  For example:
>>>> > >
>>>> > >  CREATE TABLE t(a int);
>>>> > >  CREATE POLICY p_true ON t USING (true);
>>>> > >  SELECT ddl FROM pg_get_policy_ddl('t', 'p_true', 'pretty',
>>>> 'false') AS ddl;
>>>> > >
>>>> > >  returns:
>>>> > >
>>>> > >  CREATE POLICY p_true ON public.t USING true;
>>>> > >
>>>> > >  If I drop the policy and execute that generated statement, it
>>>> fails:
>>>> > >
>>>> > >  ERROR:  syntax error at or near "true"
>>>> > >  LINE 1: CREATE POLICY p_true ON public.t USING true;
>>>> > >                                                 ^
>>>> > >
>>>> > >  The same issue reproduces for WITH CHECK:
>>>> > >
>>>> > >  CREATE POLICY p_check ON t FOR INSERT WITH CHECK (false);
>>>> > >
>>>> > >  is reconstructed as:
>>>> > >
>>>> > >  CREATE POLICY p_check ON public.t FOR INSERT WITH CHECK false;
>>>> > >
>>>> > >  and executing it fails at "false".
>>>> > >
>>>> > >  So I think USING and WITH CHECK need to be parenthesized in
>>>> non-pretty mode
>>>> > >  too, or the tests should include a round-trip execution check for
>>>> generated
>>>> > >  DDL with simple boolean expressions.
>>>> > >
>>>> > >  I used two small SQL reproducers for the manual checks; the
>>>> complete repro is
>>>> > >  included above.
>>>> > >
>>>> > >  I have not reviewed the broader pg_get_*_ddl API design or every
>>>> possible
>>>> > >  policy expression form.
>>>> > >
>>>> > >  Regards,
>>>> > >  Ilmar Yunusov
>>>> > >
>>>> > >  The new status of this patch is: Waiting on Author
>>>> >
>>>>
>>>>
>>>> I reviewed and tested the V11 pg_get_policy_ddl() patch. I verified
>>>> the basic functionality by creating various row-level security
>>>> policies and checking the reconstructed DDL returned by
>>>> pg_get_policy_ddl(). The function correctly reconstructed policies for
>>>> different command types (SELECT, INSERT, UPDATE, DELETE), PERMISSIVE
>>>> and RESTRICTIVE policies, multiple role lists, quoted identifiers,
>>>> USING clauses, and WITH CHECK clauses.
>>>> I also tested more complex cases involving subqueries. Policies
>>>> containing EXISTS subqueries in both USING and WITH CHECK clauses were
>>>> reconstructed successfully. Nested subqueries were also handled
>>>> correctly, and I did not encounter any issues related to expression
>>>> reconstruction or variable handling. I verified NULL input handling as
>>>> well. Passing NULL values for the table name or policy name returned
>>>> no rows as expected. Invalid relation names resulted in the expected
>>>> regclass resolution error. One observation from testing is that the
>>>> generated DDL omits default clauses such as FOR ALL and TO PUBLIC. For
>>>> example, a policy created with FOR ALL TO PUBLIC is reconstructed
>>>> without those clauses, while still producing semantically equivalent
>>>> DDL. I'm not sure whether this is intentional or if the function is
>>>> expected to reproduce all clauses explicitly, but it may be worth
>>>> discussing.
>>>> Overall, the patch worked as expected in all scenarios I tested, and I
>>>> did not find any functional issues.
>>>>
>>>
>>> Thanks for the review. The omission of FOR ALL and TO PUBLIC is
>>> intentional and matches the long-standing convention used by
>>> pg_get_indexdef, pg_get_constraintdef, pg_get_viewdef, etc.: emit a clause
>>> only when it differs from the parser's default. The result is semantically
>>> equivalent to the CREATE POLICY DDL.
>>>
>>>>
>>>>
>>>> Regards,
>>>> Solai
>>>>
>>>


Attachments:

  [application/octet-stream] v14-0001-Add-pg_get_policy_ddl-to-reconstruct-CREATE.patch (31.5K, ../../CANxoLDcQyd=ojcmLCmU5zFT8j475AbXHpjSWHoVP4a5-vdY3Jw@mail.gmail.com/3-v14-0001-Add-pg_get_policy_ddl-to-reconstruct-CREATE.patch)
  download | inline diff:
From 9b2d4e22060b80ead0ac33cbf8122463ba972573 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 22 May 2026 18:18:07 +0530
Subject: [PATCH v14] Add pg_get_policy_ddl() to reconstruct CREATE POLICY
 statements

  pg_get_policy_ddl(table regclass,
                    policyname name,
                    pretty bool DEFAULT false)
  RETURNS SETOF text

reconstructs the CREATE POLICY statement for the named row-level
security policy on the specified table.  Although a single statement is
produced, the function returns a SETOF text result so its calling
convention matches the rest of the pg_get_*_ddl family.

The reconstructed DDL includes all clauses of the CREATE POLICY syntax:
the policy's permissiveness (AS RESTRICTIVE), command type (FOR
SELECT/INSERT/UPDATE/DELETE), role list (TO <roles>), USING
qualification, and WITH CHECK expression.  Clauses whose value equals
the parser's default -- PERMISSIVE, FOR ALL, and TO PUBLIC -- are
omitted from the output, matching the convention used by pg_get_indexdef,
pg_get_constraintdef, pg_get_viewdef, and similar functions.  The result
is therefore semantically equivalent to the original DDL, not lexically
identical.

The pretty parameter controls output formatting and defaults to false,
following the same convention as pg_get_role_ddl, pg_get_tablespace_ddl,
and pg_get_database_ddl.  NULL passed explicitly for pretty is treated
as false.

NULL inputs for the table or policy name yield no rows.  An invalid
relation name surfaces the regclass resolution error; a non-existent
policy raises an explicit "policy ... does not exist" error.

Usage examples:

  -- compact form (default)
  SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1');
  SELECT * FROM pg_get_policy_ddl(16564, 'pol1');

  -- pretty-printed form
  SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1', true);

Regression coverage is added to src/test/regress/sql/rowsecurity.sql
and exercises all valid combinations of the CREATE POLICY syntax:
PERMISSIVE/RESTRICTIVE, all FOR command variants (ALL/SELECT/INSERT/
UPDATE/DELETE), multi-role TO lists, USING-only, WITH CHECK-only, and
USING+WITH CHECK policies for both ALL and UPDATE commands (the only
two that accept both), RESTRICTIVE on a specific command type,
subquery expressions, pretty and non-pretty output, all boolean
representations for the pretty argument (true/false, on/off, 1/0),
NULL and error paths, and a round-trip test that drops and re-executes
the generated DDL.

Author: Akshay Joshi <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  19 ++
 src/backend/utils/adt/ddlutils.c          | 259 +++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   6 +
 src/test/regress/expected/rowsecurity.out | 291 ++++++++++++++++++++++
 src/test/regress/sql/rowsecurity.sql      | 128 ++++++++++
 5 files changed, 703 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 69ef3857cfa..858ea4609a6 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3960,6 +3960,25 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
         is false, the <literal>OWNER</literal> clause is omitted.
        </para></entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>,
+        <parameter>policy_name</parameter> <type>name</type>
+        <optional>, <parameter>pretty</parameter> <type>boolean</type>
+        </optional> )
+        <returnvalue>setof text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement for the
+        named row-level security policy on the specified table.  The result
+        is returned as a single row.  If <parameter>pretty</parameter> is
+        true, the output is formatted for readability; the default is false.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c
index a70f1c28655..cf7b596085f 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_tablespace.h"
 #include "commands/tablespace.h"
 #include "common/relpath.h"
@@ -56,6 +57,9 @@ static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner
 static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid);
 static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
 										  bool no_owner, bool no_tablespace);
+static List *pg_get_policy_ddl_internal(Oid tableID, const char *policyName,
+										bool pretty);
+static const char *get_policy_cmd_name(char cmd);
 
 
 /*
@@ -975,3 +979,258 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
 		SRF_RETURN_DONE(funcctx);
 	}
 }
+
+/*
+ * get_policy_cmd_name
+ *		Map a non-ALL pg_policy.polcmd char to its SQL keyword.
+ *
+ * Callers must guard against '*' (ALL) themselves; it is not passed here.
+ */
+static const char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command: %d", (int) cmd);
+			return NULL;		/* keep compiler quiet */
+	}
+}
+
+/*
+ * pg_get_policy_ddl_internal
+ *		Generate the DDL statement to recreate a row-level security policy.
+ *
+ * Returns a List containing a single palloc'd string with the CREATE POLICY
+ * statement.  Returning a List keeps the calling convention consistent with
+ * the rest of the pg_get_*_ddl family even though only one row is produced.
+ */
+static List *
+pg_get_policy_ddl_internal(Oid tableID, const char *policyName, bool pretty)
+{
+	Relation	pgPolicyRel;
+	HeapTuple	tuplePolicy;
+	Form_pg_policy policyForm;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	StringInfoData buf;
+	Datum		valueDatum;
+	bool		attrIsNull;
+	char	   *relname;
+	char	   *nspname;
+	char	   *targetTable;
+	List	   *statements = NIL;
+
+	/* Validate that the relation exists and build its qualified name. */
+	relname = get_rel_name(tableID);
+	if (relname == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_TABLE),
+				 errmsg("relation with OID %u does not exist", tableID)));
+
+	nspname = get_namespace_name(get_rel_namespace(tableID));
+	if (nspname == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_SCHEMA),
+				 errmsg("schema for relation with OID %u does not exist",
+						tableID)));
+
+	targetTable = quote_qualified_identifier(nspname, relname);
+	pfree(relname);
+	pfree(nspname);
+
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(policyName));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						policyName, targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	initStringInfo(&buf);
+
+	/* Build the CREATE POLICY statement */
+	appendStringInfo(&buf, "CREATE POLICY %s ON %s",
+					 quote_identifier(policyName),
+					 targetTable);
+	pfree(targetTable);
+
+	/*
+	 * Emit AS RESTRICTIVE only when it differs from the default (PERMISSIVE).
+	 */
+	if (!policyForm->polpermissive)
+		append_ddl_option(&buf, pretty, 4, "AS RESTRICTIVE");
+
+	/*
+	 * Emit FOR <cmd> only when it differs from the default (ALL, encoded as
+	 * '*').
+	 */
+	if (policyForm->polcmd != '*')
+		append_ddl_option(&buf, pretty, 4, "FOR %s",
+						  get_policy_cmd_name(policyForm->polcmd));
+
+	/*
+	 * Emit TO <roles> only when it differs from the default (PUBLIC).  PUBLIC
+	 * is encoded in polroles as a single InvalidOid element, so we omit the
+	 * clause whenever every entry is InvalidOid.  polroles is always non-NULL.
+	 */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	Assert(!attrIsNull);
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypeP(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (role_names.len > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, quote_identifier(rolename));
+				pfree(rolename);
+			}
+		}
+
+		if (role_names.len > 0)
+			append_ddl_option(&buf, pretty, 4, "TO %s", role_names.data);
+
+		pfree(role_names.data);
+	}
+
+	/* USING expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "USING (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	/* WITH CHECK expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "WITH CHECK (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	appendStringInfoChar(&buf, ';');
+
+	statements = lappend(statements, pstrdup(buf.data));
+
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+	pfree(buf.data);
+
+	return statements;
+}
+
+/*
+ * pg_get_policy_ddl
+ *		Return DDL to recreate a row-level security policy as a single text row.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	List	   *statements;
+
+	if (SRF_IS_FIRSTCALL())
+	{
+		MemoryContext oldcontext;
+		Oid			tableID;
+		Name		policyName;
+		bool		pretty;
+
+		funcctx = SRF_FIRSTCALL_INIT();
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+		{
+			MemoryContextSwitchTo(oldcontext);
+			SRF_RETURN_DONE(funcctx);
+		}
+
+		tableID = PG_GETARG_OID(0);
+		policyName = PG_GETARG_NAME(1);
+		pretty = !PG_ARGISNULL(2) && PG_GETARG_BOOL(2);
+
+		statements = pg_get_policy_ddl_internal(tableID,
+												NameStr(*policyName),
+												pretty);
+		funcctx->user_fctx = statements;
+		funcctx->max_calls = list_length(statements);
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	funcctx = SRF_PERCALL_SETUP();
+	statements = (List *) funcctx->user_fctx;
+
+	if (funcctx->call_cntr < funcctx->max_calls)
+	{
+		char	   *stmt;
+
+		stmt = list_nth(statements, funcctx->call_cntr);
+
+		SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+	}
+	else
+	{
+		list_free_deep(statements);
+		SRF_RETURN_DONE(funcctx);
+	}
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 73bb7fbb430..9c46f3676de 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8620,6 +8620,12 @@
   proargtypes => 'regdatabase bool bool bool',
   proargnames => '{database,pretty,owner,tablespace}',
   proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' },
+{ oid => '6517', descr => 'get DDL to recreate a row-level security policy',
+  proname => 'pg_get_policy_ddl', prorows => '1', proisstrict => 'f',
+  proretset => 't', provolatile => 's', pronargdefaults => '1',
+  prorettype => 'text', proargtypes => 'regclass name bool',
+  proargnames => '{table,policyname,pretty}', proargdefaults => '{false}',
+  prosrc => 'pg_get_policy_ddl' },
 { oid => '2509',
   descr => 'deparse an encoded expression with pretty-print option',
   proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 3a5e82c35bd..bf54f66339f 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -5195,6 +5195,297 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(table, policy_name[, pretty]) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- Test UPDATE policy with both USING and WITH CHECK
+CREATE POLICY rls_p10 ON rls_tbl_1 FOR UPDATE
+    USING (dlevel > 0) WITH CHECK (dlevel < 100);
+-- Test ALL policy with both USING and WITH CHECK (AS PERMISSIVE and FOR ALL are defaults, omitted)
+CREATE POLICY rls_p11 ON rls_tbl_1
+    USING (dlevel >= 1) WITH CHECK (dlevel <= 99);
+-- Test RESTRICTIVE on a specific command
+CREATE POLICY rls_p12 ON rls_tbl_1 AS RESTRICTIVE FOR SELECT
+    USING (cid > 0);
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+ count 
+-------
+     0
+(1 row)
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+ERROR:  relation "nonexistent_tbl" does not exist
+LINE 1: SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1')...
+                                        ^
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+ERROR:  policy "nonexistent_pol" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Without pretty formatting (default); also verify explicit false equals default
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                              +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+                                   pg_get_policy_ddl                                    
+----------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 FOR SELECT USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+                                       pg_get_policy_ddl                                       
+-----------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 FOR INSERT WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7');
+                                 pg_get_policy_ddl                                  
+------------------------------------------------------------------------------------
+ CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 FOR DELETE USING ((cid < 8));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9');
+                                                      pg_get_policy_ddl                                                      
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 TO regress_rls_exempt_user WITH CHECK ((cid = ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2)));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10');
+                                                 pg_get_policy_ddl                                                  
+--------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p10 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING ((dlevel > 0)) WITH CHECK ((dlevel < 100));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p11');
+                                            pg_get_policy_ddl                                             
+----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p11 ON regress_rls_schema.rls_tbl_1 USING ((dlevel >= 1)) WITH CHECK ((dlevel <= 99));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p12');
+                                         pg_get_policy_ddl                                          
+----------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p12 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE FOR SELECT USING ((cid > 0));
+(1 row)
+
+-- Explicit false must match omitting the argument
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', false);
+                                                 pg_get_policy_ddl                                                  
+--------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p10 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING ((dlevel > 0)) WITH CHECK ((dlevel < 100));
+(1 row)
+
+-- NULL pretty must also default to non-pretty
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', NULL);
+                                                 pg_get_policy_ddl                                                  
+--------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p10 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING ((dlevel > 0)) WITH CHECK ((dlevel < 100));
+(1 row)
+
+-- pretty accepts all standard boolean representations: true/false, on/off, 1/0
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'on'::bool);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', '1'::bool);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'off'::bool);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', '0'::bool);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+\pset format aligned
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+    AS RESTRICTIVE
+    USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+    FOR SELECT
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+    FOR INSERT
+    WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+    FOR UPDATE
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+    FOR DELETE
+    USING (cid < 8);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_dave, regress_rls_alice
+    USING (true);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_exempt_user
+    WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p10 ON regress_rls_schema.rls_tbl_1
+    FOR UPDATE
+    USING (dlevel > 0)
+    WITH CHECK (dlevel < 100);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p11', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p11 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel >= 1)
+    WITH CHECK (dlevel <= 99);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p12', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p12 ON regress_rls_schema.rls_tbl_1
+    AS RESTRICTIVE
+    FOR SELECT
+    USING (cid > 0);
+(1 row)
+\pset format aligned
+-- Round-trip: the generated DDL must be re-executable, including for atomic
+-- boolean expressions that pg_get_expr() does not parenthesize.
+CREATE TABLE rls_rt (a int);
+CREATE POLICY rt_true ON rls_rt USING (true);
+CREATE POLICY rt_false ON rls_rt FOR INSERT WITH CHECK (false);
+CREATE TEMP TABLE rt_ddl AS
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_true') AS ddl
+    UNION ALL
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_false');
+DROP POLICY rt_true ON rls_rt;
+DROP POLICY rt_false ON rls_rt;
+SELECT ddl FROM rt_ddl ORDER BY ddl \gexec
+CREATE POLICY rt_false ON regress_rls_schema.rls_rt FOR INSERT WITH CHECK (false);
+CREATE POLICY rt_true ON regress_rls_schema.rls_rt USING (true);
+SELECT polname FROM pg_policy WHERE polrelid = 'rls_rt'::regclass ORDER BY polname;
+ polname  
+----------
+ rt_false
+ rt_true
+(2 rows)
+
+DROP TABLE rls_rt;
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP POLICY rls_p10 ON rls_tbl_1;
+DROP POLICY rls_p11 ON rls_tbl_1;
+DROP POLICY rls_p12 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 6b3566271df..e4d64813e65 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2598,6 +2598,134 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(table, policy_name[, pretty]) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- Test UPDATE policy with both USING and WITH CHECK
+CREATE POLICY rls_p10 ON rls_tbl_1 FOR UPDATE
+    USING (dlevel > 0) WITH CHECK (dlevel < 100);
+
+-- Test ALL policy with both USING and WITH CHECK (AS PERMISSIVE and FOR ALL are defaults, omitted)
+CREATE POLICY rls_p11 ON rls_tbl_1
+    USING (dlevel >= 1) WITH CHECK (dlevel <= 99);
+
+-- Test RESTRICTIVE on a specific command
+CREATE POLICY rls_p12 ON rls_tbl_1 AS RESTRICTIVE FOR SELECT
+    USING (cid > 0);
+
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+
+-- Without pretty formatting (default); also verify explicit false equals default
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p11');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p12');
+-- Explicit false must match omitting the argument
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', false);
+-- NULL pretty must also default to non-pretty
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', NULL);
+
+-- pretty accepts all standard boolean representations: true/false, on/off, 1/0
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'on'::bool);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', '1'::bool);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'off'::bool);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', '0'::bool);
+\pset format aligned
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p11', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p12', true);
+\pset format aligned
+
+-- Round-trip: the generated DDL must be re-executable, including for atomic
+-- boolean expressions that pg_get_expr() does not parenthesize.
+CREATE TABLE rls_rt (a int);
+CREATE POLICY rt_true ON rls_rt USING (true);
+CREATE POLICY rt_false ON rls_rt FOR INSERT WITH CHECK (false);
+CREATE TEMP TABLE rt_ddl AS
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_true') AS ddl
+    UNION ALL
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_false');
+DROP POLICY rt_true ON rls_rt;
+DROP POLICY rt_false ON rls_rt;
+SELECT ddl FROM rt_ddl ORDER BY ddl \gexec
+SELECT polname FROM pg_policy WHERE polrelid = 'rls_rt'::regclass ORDER BY polname;
+DROP TABLE rls_rt;
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP POLICY rls_p10 ON rls_tbl_1;
+DROP POLICY rls_p11 ON rls_tbl_1;
+DROP POLICY rls_p12 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-07-01 09:15  solai v <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: solai v @ 2026-07-01 09:15 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Ilmar Y <[email protected]>; [email protected]

Hi all,


On Tue, Jun 30, 2026 at 4:48 PM Akshay Joshi
<[email protected]> wrote:
>
> All,
>
> I've updated the patch to align with the interface change introduced by commit d6ed87d1989 (Andrew Dunstan, 2026-06-26 "Use named boolean parameters for pg_get_*_ddl option arguments").
>
> That commit replaced the VARIADIC text[] alternating key/value option interface with typed named boolean parameters across pg_get_role_ddl(), pg_get_tablespace_ddl(), and pg_get_database_ddl(), removing the DdlOption/parse_ddl_options() machinery in favour of direct PG_GETARG_BOOL() calls.
>
> Updated patch v14 is ready for review/commit.
>


I reviewed and tested the v14 patch on the latest master. The patch
applied cleanly, built successfully, and passed make check without any
regression failures. I verified the new function signature and
confirmed that pg_get_policy_ddl() now uses the new interface with the
boolean DEFAULT false argument. Also I tested the function with a
variety of row-level security policies, including: Basic policy
reconstruction, PERMISSIVE and RESTRICTIVE policies, Different command
types (SELECT, INSERT, UPDATE, DELETE), Policies with multiple roles
and the PUBLIC role, Quoted table, policy, and role names, USING and
WITH CHECK clauses, Complex expressions including EXISTS, nested
subqueries, CASE expressions, COALESCE, ANY/ALL operators, and boolean
expressions, NULL inputs, invalid relation names, and non-existent
policies. The generated DDL was correct as per the intent, and the
expected errors were verified for the invalid inputs. I also verified
that the generated DDL is executable by dropping an existing policy,
recreating it using the output of pg_get_policy_ddl(), and confirming
that the recreated policy was reconstructed successfully again by the
function.
One observation I noted is that for policies using default attributes
(TO PUBLIC and AS PERMISSIVE), the generated DDL omits those clauses
and produces a semantically equivalent statement which seems
intentional but thought of sending it here for further clarification.
Overall, I did not encounter any functional issues during testing. The
patch looks good to me and worked as expected in all the scenarios I
tested.


Regards,
Solai





^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-07-01 10:31  Akshay Joshi <[email protected]>
  parent: solai v <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2026-07-01 10:31 UTC (permalink / raw)
  To: solai v <[email protected]>; +Cc: Ilmar Y <[email protected]>; [email protected]

On Wed, Jul 1, 2026 at 2:44 PM solai v <[email protected]> wrote:

> Hi all,
>
>
> On Tue, Jun 30, 2026 at 4:48 PM Akshay Joshi
> <[email protected]> wrote:
> >
> > All,
> >
> > I've updated the patch to align with the interface change introduced by
> commit d6ed87d1989 (Andrew Dunstan, 2026-06-26 "Use named boolean
> parameters for pg_get_*_ddl option arguments").
> >
> > That commit replaced the VARIADIC text[] alternating key/value option
> interface with typed named boolean parameters across pg_get_role_ddl(),
> pg_get_tablespace_ddl(), and pg_get_database_ddl(), removing the
> DdlOption/parse_ddl_options() machinery in favour of direct
> PG_GETARG_BOOL() calls.
> >
> > Updated patch v14 is ready for review/commit.
> >
>
>
> I reviewed and tested the v14 patch on the latest master. The patch
> applied cleanly, built successfully, and passed make check without any
> regression failures. I verified the new function signature and
> confirmed that pg_get_policy_ddl() now uses the new interface with the
> boolean DEFAULT false argument. Also I tested the function with a
> variety of row-level security policies, including: Basic policy
> reconstruction, PERMISSIVE and RESTRICTIVE policies, Different command
> types (SELECT, INSERT, UPDATE, DELETE), Policies with multiple roles
> and the PUBLIC role, Quoted table, policy, and role names, USING and
> WITH CHECK clauses, Complex expressions including EXISTS, nested
> subqueries, CASE expressions, COALESCE, ANY/ALL operators, and boolean
> expressions, NULL inputs, invalid relation names, and non-existent
> policies. The generated DDL was correct as per the intent, and the
> expected errors were verified for the invalid inputs. I also verified
> that the generated DDL is executable by dropping an existing policy,
> recreating it using the output of pg_get_policy_ddl(), and confirming
> that the recreated policy was reconstructed successfully again by the
> function.
> One observation I noted is that for policies using default attributes
> (TO PUBLIC and AS PERMISSIVE), the generated DDL omits those clauses
> and produces a semantically equivalent statement which seems
> intentional but thought of sending it here for further clarification.
> Overall, I did not encounter any functional issues during testing. The
> patch looks good to me and worked as expected in all the scenarios I
> tested.
>

    Yes, it's intentional for all pg_get_***_ddl functions. Clause with
defaults won't be reconstructed.

>
>
> Regards,
> Solai
>


^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-07-01 11:31  solai v <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: solai v @ 2026-07-01 11:31 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Ilmar Y <[email protected]>; [email protected]

Hi all,

On Wed, Jul 1, 2026 at 4:01 PM Akshay Joshi
<[email protected]> wrote:
>
>
>
> On Wed, Jul 1, 2026 at 2:44 PM solai v <[email protected]> wrote:
>>
>> Hi all,
>>
>>
>> On Tue, Jun 30, 2026 at 4:48 PM Akshay Joshi
>> <[email protected]> wrote:
>> >
>> > All,
>> >
>> > I've updated the patch to align with the interface change introduced by commit d6ed87d1989 (Andrew Dunstan, 2026-06-26 "Use named boolean parameters for pg_get_*_ddl option arguments").
>> >
>> > That commit replaced the VARIADIC text[] alternating key/value option interface with typed named boolean parameters across pg_get_role_ddl(), pg_get_tablespace_ddl(), and pg_get_database_ddl(), removing the DdlOption/parse_ddl_options() machinery in favour of direct PG_GETARG_BOOL() calls.
>> >
>> > Updated patch v14 is ready for review/commit.
>> >
>>
>>
>> I reviewed and tested the v14 patch on the latest master. The patch
>> applied cleanly, built successfully, and passed make check without any
>> regression failures. I verified the new function signature and
>> confirmed that pg_get_policy_ddl() now uses the new interface with the
>> boolean DEFAULT false argument. Also I tested the function with a
>> variety of row-level security policies, including: Basic policy
>> reconstruction, PERMISSIVE and RESTRICTIVE policies, Different command
>> types (SELECT, INSERT, UPDATE, DELETE), Policies with multiple roles
>> and the PUBLIC role, Quoted table, policy, and role names, USING and
>> WITH CHECK clauses, Complex expressions including EXISTS, nested
>> subqueries, CASE expressions, COALESCE, ANY/ALL operators, and boolean
>> expressions, NULL inputs, invalid relation names, and non-existent
>> policies. The generated DDL was correct as per the intent, and the
>> expected errors were verified for the invalid inputs. I also verified
>> that the generated DDL is executable by dropping an existing policy,
>> recreating it using the output of pg_get_policy_ddl(), and confirming
>> that the recreated policy was reconstructed successfully again by the
>> function.
>> One observation I noted is that for policies using default attributes
>> (TO PUBLIC and AS PERMISSIVE), the generated DDL omits those clauses
>> and produces a semantically equivalent statement which seems
>> intentional but thought of sending it here for further clarification.
>> Overall, I did not encounter any functional issues during testing. The
>> patch looks good to me and worked as expected in all the scenarios I
>> tested.
>
>
>     Yes, it's intentional for all pg_get_***_ddl functions. Clause with defaults won't be reconstructed.
>>
>>

Ok. Thank you for the clarification.

Regards,
Solai






^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-07-06 11:28  Akshay Joshi <[email protected]>
  parent: solai v <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Akshay Joshi @ 2026-07-06 11:28 UTC (permalink / raw)
  To: solai v <[email protected]>; +Cc: Ilmar Y <[email protected]>; [email protected]

All,

I found that OID 6517 was already taken by the max(uuid) aggregate. I've
reassigned pg_get_policy_ddl to OID 9683.
The v15 patch is ready for review and commit.

On Wed, Jul 1, 2026 at 5:01 PM solai v <[email protected]> wrote:

> Hi all,
>
> On Wed, Jul 1, 2026 at 4:01 PM Akshay Joshi
> <[email protected]> wrote:
> >
> >
> >
> > On Wed, Jul 1, 2026 at 2:44 PM solai v <[email protected]> wrote:
> >>
> >> Hi all,
> >>
> >>
> >> On Tue, Jun 30, 2026 at 4:48 PM Akshay Joshi
> >> <[email protected]> wrote:
> >> >
> >> > All,
> >> >
> >> > I've updated the patch to align with the interface change introduced
> by commit d6ed87d1989 (Andrew Dunstan, 2026-06-26 "Use named boolean
> parameters for pg_get_*_ddl option arguments").
> >> >
> >> > That commit replaced the VARIADIC text[] alternating key/value option
> interface with typed named boolean parameters across pg_get_role_ddl(),
> pg_get_tablespace_ddl(), and pg_get_database_ddl(), removing the
> DdlOption/parse_ddl_options() machinery in favour of direct
> PG_GETARG_BOOL() calls.
> >> >
> >> > Updated patch v14 is ready for review/commit.
> >> >
> >>
> >>
> >> I reviewed and tested the v14 patch on the latest master. The patch
> >> applied cleanly, built successfully, and passed make check without any
> >> regression failures. I verified the new function signature and
> >> confirmed that pg_get_policy_ddl() now uses the new interface with the
> >> boolean DEFAULT false argument. Also I tested the function with a
> >> variety of row-level security policies, including: Basic policy
> >> reconstruction, PERMISSIVE and RESTRICTIVE policies, Different command
> >> types (SELECT, INSERT, UPDATE, DELETE), Policies with multiple roles
> >> and the PUBLIC role, Quoted table, policy, and role names, USING and
> >> WITH CHECK clauses, Complex expressions including EXISTS, nested
> >> subqueries, CASE expressions, COALESCE, ANY/ALL operators, and boolean
> >> expressions, NULL inputs, invalid relation names, and non-existent
> >> policies. The generated DDL was correct as per the intent, and the
> >> expected errors were verified for the invalid inputs. I also verified
> >> that the generated DDL is executable by dropping an existing policy,
> >> recreating it using the output of pg_get_policy_ddl(), and confirming
> >> that the recreated policy was reconstructed successfully again by the
> >> function.
> >> One observation I noted is that for policies using default attributes
> >> (TO PUBLIC and AS PERMISSIVE), the generated DDL omits those clauses
> >> and produces a semantically equivalent statement which seems
> >> intentional but thought of sending it here for further clarification.
> >> Overall, I did not encounter any functional issues during testing. The
> >> patch looks good to me and worked as expected in all the scenarios I
> >> tested.
> >
> >
> >     Yes, it's intentional for all pg_get_***_ddl functions. Clause with
> defaults won't be reconstructed.
> >>
> >>
>
> Ok. Thank you for the clarification.
>
> Regards,
> Solai
>


Attachments:

  [application/octet-stream] v15-0001-Add-pg_get_policy_ddl-to-reconstruct-CREATE.patch (31.5K, ../../CANxoLDfUeoh+X1AeazXAQjYL312nTa6bY+=iPNJjmN4bXwSKWw@mail.gmail.com/3-v15-0001-Add-pg_get_policy_ddl-to-reconstruct-CREATE.patch)
  download | inline diff:
From 30aa1e5001737ff0ce675aa8adf8ae7e6cda633d Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 22 May 2026 18:18:07 +0530
Subject: [PATCH v15] Add pg_get_policy_ddl() to reconstruct CREATE POLICY
 statements

  pg_get_policy_ddl(table regclass,
                    policyname name,
                    pretty bool DEFAULT false)
  RETURNS SETOF text

reconstructs the CREATE POLICY statement for the named row-level
security policy on the specified table.  Although a single statement is
produced, the function returns a SETOF text result so its calling
convention matches the rest of the pg_get_*_ddl family.

The reconstructed DDL includes all clauses of the CREATE POLICY syntax:
the policy's permissiveness (AS RESTRICTIVE), command type (FOR
SELECT/INSERT/UPDATE/DELETE), role list (TO <roles>), USING
qualification, and WITH CHECK expression.  Clauses whose value equals
the parser's default -- PERMISSIVE, FOR ALL, and TO PUBLIC -- are
omitted from the output, matching the convention used by pg_get_indexdef,
pg_get_constraintdef, pg_get_viewdef, and similar functions.  The result
is therefore semantically equivalent to the original DDL, not lexically
identical.

The pretty parameter controls output formatting and defaults to false,
following the same convention as pg_get_role_ddl, pg_get_tablespace_ddl,
and pg_get_database_ddl.  NULL passed explicitly for pretty is treated
as false.

NULL inputs for the table or policy name yield no rows.  An invalid
relation name surfaces the regclass resolution error; a non-existent
policy raises an explicit "policy ... does not exist" error.

Usage examples:

  -- compact form (default)
  SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1');
  SELECT * FROM pg_get_policy_ddl(16564, 'pol1');

  -- pretty-printed form
  SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1', true);

Regression coverage is added to src/test/regress/sql/rowsecurity.sql
and exercises all valid combinations of the CREATE POLICY syntax:
PERMISSIVE/RESTRICTIVE, all FOR command variants (ALL/SELECT/INSERT/
UPDATE/DELETE), multi-role TO lists, USING-only, WITH CHECK-only, and
USING+WITH CHECK policies for both ALL and UPDATE commands (the only
two that accept both), RESTRICTIVE on a specific command type,
subquery expressions, pretty and non-pretty output, all boolean
representations for the pretty argument (true/false, on/off, 1/0),
NULL and error paths, and a round-trip test that drops and re-executes
the generated DDL.

Author: Akshay Joshi <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  19 ++
 src/backend/utils/adt/ddlutils.c          | 259 +++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   6 +
 src/test/regress/expected/rowsecurity.out | 291 ++++++++++++++++++++++
 src/test/regress/sql/rowsecurity.sql      | 128 ++++++++++
 5 files changed, 703 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 69ef3857cfa..858ea4609a6 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3960,6 +3960,25 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
         is false, the <literal>OWNER</literal> clause is omitted.
        </para></entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>,
+        <parameter>policy_name</parameter> <type>name</type>
+        <optional>, <parameter>pretty</parameter> <type>boolean</type>
+        </optional> )
+        <returnvalue>setof text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement for the
+        named row-level security policy on the specified table.  The result
+        is returned as a single row.  If <parameter>pretty</parameter> is
+        true, the output is formatted for readability; the default is false.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c
index a70f1c28655..cf7b596085f 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_tablespace.h"
 #include "commands/tablespace.h"
 #include "common/relpath.h"
@@ -56,6 +57,9 @@ static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner
 static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid);
 static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
 										  bool no_owner, bool no_tablespace);
+static List *pg_get_policy_ddl_internal(Oid tableID, const char *policyName,
+										bool pretty);
+static const char *get_policy_cmd_name(char cmd);
 
 
 /*
@@ -975,3 +979,258 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
 		SRF_RETURN_DONE(funcctx);
 	}
 }
+
+/*
+ * get_policy_cmd_name
+ *		Map a non-ALL pg_policy.polcmd char to its SQL keyword.
+ *
+ * Callers must guard against '*' (ALL) themselves; it is not passed here.
+ */
+static const char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command: %d", (int) cmd);
+			return NULL;		/* keep compiler quiet */
+	}
+}
+
+/*
+ * pg_get_policy_ddl_internal
+ *		Generate the DDL statement to recreate a row-level security policy.
+ *
+ * Returns a List containing a single palloc'd string with the CREATE POLICY
+ * statement.  Returning a List keeps the calling convention consistent with
+ * the rest of the pg_get_*_ddl family even though only one row is produced.
+ */
+static List *
+pg_get_policy_ddl_internal(Oid tableID, const char *policyName, bool pretty)
+{
+	Relation	pgPolicyRel;
+	HeapTuple	tuplePolicy;
+	Form_pg_policy policyForm;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	StringInfoData buf;
+	Datum		valueDatum;
+	bool		attrIsNull;
+	char	   *relname;
+	char	   *nspname;
+	char	   *targetTable;
+	List	   *statements = NIL;
+
+	/* Validate that the relation exists and build its qualified name. */
+	relname = get_rel_name(tableID);
+	if (relname == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_TABLE),
+				 errmsg("relation with OID %u does not exist", tableID)));
+
+	nspname = get_namespace_name(get_rel_namespace(tableID));
+	if (nspname == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_SCHEMA),
+				 errmsg("schema for relation with OID %u does not exist",
+						tableID)));
+
+	targetTable = quote_qualified_identifier(nspname, relname);
+	pfree(relname);
+	pfree(nspname);
+
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(policyName));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						policyName, targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	initStringInfo(&buf);
+
+	/* Build the CREATE POLICY statement */
+	appendStringInfo(&buf, "CREATE POLICY %s ON %s",
+					 quote_identifier(policyName),
+					 targetTable);
+	pfree(targetTable);
+
+	/*
+	 * Emit AS RESTRICTIVE only when it differs from the default (PERMISSIVE).
+	 */
+	if (!policyForm->polpermissive)
+		append_ddl_option(&buf, pretty, 4, "AS RESTRICTIVE");
+
+	/*
+	 * Emit FOR <cmd> only when it differs from the default (ALL, encoded as
+	 * '*').
+	 */
+	if (policyForm->polcmd != '*')
+		append_ddl_option(&buf, pretty, 4, "FOR %s",
+						  get_policy_cmd_name(policyForm->polcmd));
+
+	/*
+	 * Emit TO <roles> only when it differs from the default (PUBLIC).  PUBLIC
+	 * is encoded in polroles as a single InvalidOid element, so we omit the
+	 * clause whenever every entry is InvalidOid.  polroles is always non-NULL.
+	 */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	Assert(!attrIsNull);
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypeP(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (role_names.len > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, quote_identifier(rolename));
+				pfree(rolename);
+			}
+		}
+
+		if (role_names.len > 0)
+			append_ddl_option(&buf, pretty, 4, "TO %s", role_names.data);
+
+		pfree(role_names.data);
+	}
+
+	/* USING expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "USING (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	/* WITH CHECK expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "WITH CHECK (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	appendStringInfoChar(&buf, ';');
+
+	statements = lappend(statements, pstrdup(buf.data));
+
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+	pfree(buf.data);
+
+	return statements;
+}
+
+/*
+ * pg_get_policy_ddl
+ *		Return DDL to recreate a row-level security policy as a single text row.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	List	   *statements;
+
+	if (SRF_IS_FIRSTCALL())
+	{
+		MemoryContext oldcontext;
+		Oid			tableID;
+		Name		policyName;
+		bool		pretty;
+
+		funcctx = SRF_FIRSTCALL_INIT();
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+		{
+			MemoryContextSwitchTo(oldcontext);
+			SRF_RETURN_DONE(funcctx);
+		}
+
+		tableID = PG_GETARG_OID(0);
+		policyName = PG_GETARG_NAME(1);
+		pretty = !PG_ARGISNULL(2) && PG_GETARG_BOOL(2);
+
+		statements = pg_get_policy_ddl_internal(tableID,
+												NameStr(*policyName),
+												pretty);
+		funcctx->user_fctx = statements;
+		funcctx->max_calls = list_length(statements);
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	funcctx = SRF_PERCALL_SETUP();
+	statements = (List *) funcctx->user_fctx;
+
+	if (funcctx->call_cntr < funcctx->max_calls)
+	{
+		char	   *stmt;
+
+		stmt = list_nth(statements, funcctx->call_cntr);
+
+		SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+	}
+	else
+	{
+		list_free_deep(statements);
+		SRF_RETURN_DONE(funcctx);
+	}
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..cd029b483f1 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8634,6 +8634,12 @@
   proargtypes => 'regdatabase bool bool bool',
   proargnames => '{database,pretty,owner,tablespace}',
   proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' },
+{ oid => '9683', descr => 'get DDL to recreate a row-level security policy',
+  proname => 'pg_get_policy_ddl', prorows => '1', proisstrict => 'f',
+  proretset => 't', provolatile => 's', pronargdefaults => '1',
+  prorettype => 'text', proargtypes => 'regclass name bool',
+  proargnames => '{table,policyname,pretty}', proargdefaults => '{false}',
+  prosrc => 'pg_get_policy_ddl' },
 { oid => '2509',
   descr => 'deparse an encoded expression with pretty-print option',
   proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 3a5e82c35bd..bf54f66339f 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -5195,6 +5195,297 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(table, policy_name[, pretty]) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- Test UPDATE policy with both USING and WITH CHECK
+CREATE POLICY rls_p10 ON rls_tbl_1 FOR UPDATE
+    USING (dlevel > 0) WITH CHECK (dlevel < 100);
+-- Test ALL policy with both USING and WITH CHECK (AS PERMISSIVE and FOR ALL are defaults, omitted)
+CREATE POLICY rls_p11 ON rls_tbl_1
+    USING (dlevel >= 1) WITH CHECK (dlevel <= 99);
+-- Test RESTRICTIVE on a specific command
+CREATE POLICY rls_p12 ON rls_tbl_1 AS RESTRICTIVE FOR SELECT
+    USING (cid > 0);
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+ count 
+-------
+     0
+(1 row)
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+ERROR:  relation "nonexistent_tbl" does not exist
+LINE 1: SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1')...
+                                        ^
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+ERROR:  policy "nonexistent_pol" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Without pretty formatting (default); also verify explicit false equals default
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2                                                                              +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+                                   pg_get_policy_ddl                                    
+----------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 FOR SELECT USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+                                       pg_get_policy_ddl                                       
+-----------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 FOR INSERT WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7');
+                                 pg_get_policy_ddl                                  
+------------------------------------------------------------------------------------
+ CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 FOR DELETE USING ((cid < 8));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9');
+                                                      pg_get_policy_ddl                                                      
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 TO regress_rls_exempt_user WITH CHECK ((cid = ( SELECT rls_tbl_2.seclv+
+    FROM rls_tbl_2)));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10');
+                                                 pg_get_policy_ddl                                                  
+--------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p10 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING ((dlevel > 0)) WITH CHECK ((dlevel < 100));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p11');
+                                            pg_get_policy_ddl                                             
+----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p11 ON regress_rls_schema.rls_tbl_1 USING ((dlevel >= 1)) WITH CHECK ((dlevel <= 99));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p12');
+                                         pg_get_policy_ddl                                          
+----------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p12 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE FOR SELECT USING ((cid > 0));
+(1 row)
+
+-- Explicit false must match omitting the argument
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', false);
+                                                 pg_get_policy_ddl                                                  
+--------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p10 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING ((dlevel > 0)) WITH CHECK ((dlevel < 100));
+(1 row)
+
+-- NULL pretty must also default to non-pretty
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', NULL);
+                                                 pg_get_policy_ddl                                                  
+--------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p10 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING ((dlevel > 0)) WITH CHECK ((dlevel < 100));
+(1 row)
+
+-- pretty accepts all standard boolean representations: true/false, on/off, 1/0
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'on'::bool);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', '1'::bool);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'off'::bool);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', '0'::bool);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+\pset format aligned
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+    AS RESTRICTIVE
+    USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+    FOR SELECT
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+    FOR INSERT
+    WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+    FOR UPDATE
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+    FOR DELETE
+    USING (cid < 8);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_dave, regress_rls_alice
+    USING (true);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_exempt_user
+    WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM rls_tbl_2)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p10 ON regress_rls_schema.rls_tbl_1
+    FOR UPDATE
+    USING (dlevel > 0)
+    WITH CHECK (dlevel < 100);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p11', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p11 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel >= 1)
+    WITH CHECK (dlevel <= 99);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p12', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p12 ON regress_rls_schema.rls_tbl_1
+    AS RESTRICTIVE
+    FOR SELECT
+    USING (cid > 0);
+(1 row)
+\pset format aligned
+-- Round-trip: the generated DDL must be re-executable, including for atomic
+-- boolean expressions that pg_get_expr() does not parenthesize.
+CREATE TABLE rls_rt (a int);
+CREATE POLICY rt_true ON rls_rt USING (true);
+CREATE POLICY rt_false ON rls_rt FOR INSERT WITH CHECK (false);
+CREATE TEMP TABLE rt_ddl AS
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_true') AS ddl
+    UNION ALL
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_false');
+DROP POLICY rt_true ON rls_rt;
+DROP POLICY rt_false ON rls_rt;
+SELECT ddl FROM rt_ddl ORDER BY ddl \gexec
+CREATE POLICY rt_false ON regress_rls_schema.rls_rt FOR INSERT WITH CHECK (false);
+CREATE POLICY rt_true ON regress_rls_schema.rls_rt USING (true);
+SELECT polname FROM pg_policy WHERE polrelid = 'rls_rt'::regclass ORDER BY polname;
+ polname  
+----------
+ rt_false
+ rt_true
+(2 rows)
+
+DROP TABLE rls_rt;
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP POLICY rls_p10 ON rls_tbl_1;
+DROP POLICY rls_p11 ON rls_tbl_1;
+DROP POLICY rls_p12 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 6b3566271df..e4d64813e65 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2598,6 +2598,134 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(table, policy_name[, pretty]) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- Test UPDATE policy with both USING and WITH CHECK
+CREATE POLICY rls_p10 ON rls_tbl_1 FOR UPDATE
+    USING (dlevel > 0) WITH CHECK (dlevel < 100);
+
+-- Test ALL policy with both USING and WITH CHECK (AS PERMISSIVE and FOR ALL are defaults, omitted)
+CREATE POLICY rls_p11 ON rls_tbl_1
+    USING (dlevel >= 1) WITH CHECK (dlevel <= 99);
+
+-- Test RESTRICTIVE on a specific command
+CREATE POLICY rls_p12 ON rls_tbl_1 AS RESTRICTIVE FOR SELECT
+    USING (cid > 0);
+
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+
+-- Without pretty formatting (default); also verify explicit false equals default
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p11');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p12');
+-- Explicit false must match omitting the argument
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', false);
+-- NULL pretty must also default to non-pretty
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', NULL);
+
+-- pretty accepts all standard boolean representations: true/false, on/off, 1/0
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'on'::bool);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', '1'::bool);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'off'::bool);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', '0'::bool);
+\pset format aligned
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p11', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p12', true);
+\pset format aligned
+
+-- Round-trip: the generated DDL must be re-executable, including for atomic
+-- boolean expressions that pg_get_expr() does not parenthesize.
+CREATE TABLE rls_rt (a int);
+CREATE POLICY rt_true ON rls_rt USING (true);
+CREATE POLICY rt_false ON rls_rt FOR INSERT WITH CHECK (false);
+CREATE TEMP TABLE rt_ddl AS
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_true') AS ddl
+    UNION ALL
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_false');
+DROP POLICY rt_true ON rls_rt;
+DROP POLICY rt_false ON rls_rt;
+SELECT ddl FROM rt_ddl ORDER BY ddl \gexec
+SELECT polname FROM pg_policy WHERE polrelid = 'rls_rt'::regclass ORDER BY polname;
+DROP TABLE rls_rt;
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP POLICY rls_p10 ON rls_tbl_1;
+DROP POLICY rls_p11 ON rls_tbl_1;
+DROP POLICY rls_p12 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-07-08 16:58  Rui Zhao <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 44+ messages in thread

From: Rui Zhao @ 2026-07-08 16:58 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: solai v <[email protected]>; Ilmar Y <[email protected]>; [email protected]

Hi Akshay,

I tested v15 on current master (57f93af36f): builds, make check passes, and
basic reconstruction, pretty mode, NULL handling, and default-clause omission
all look good. Two things.

1) Back in the v3/v4 discussion you decided the ON <table> name should always
be schema-qualified for safety (the pg_get_triggerdef_worker thread with
jian he), and that pretty here only controls formatting, not schema (Phil's
point). That reasoning didn't reach the USING / WITH CHECK expressions,
though: object references inside them are still qualified only by the caller's
search_path, so they can lose their schema and rebind to a different object
when the DDL is replayed elsewhere:

    CREATE FUNCTION s1.f(int) RETURNS bool LANGUAGE sql AS 'SELECT $1 > 0';
    CREATE FUNCTION s2.f(int) RETURNS bool LANGUAGE sql AS 'SELECT $1 < 0';
    CREATE POLICY pf ON t2 USING (s1.f(a));

    SET search_path = public, s1;
    SELECT ddl FROM pg_get_policy_ddl('t2', 'pf') AS ddl;
    --  CREATE POLICY pf ON public.t2 USING (f(a));      -- s1. dropped

    SET search_path = public, s2;
    CREATE POLICY pf ON t2 USING (f(a));                 -- now s2.f,
opposite meaning

So within one statement the ON clause is always qualified but the expression
body isn't -- and as Phil noted, the pretty flag can't fix this, since
pg_get_expr qualifies by search_path visibility regardless of pretty.

Worth settling the contract, given where this function sits. The old
pg_get_viewdef / ruledef / indexdef functions are search_path-aware and leave
qualification to the caller (pg_dump sets search_path empty around them so the
output is portable). The new pg_get_*_ddl functions committed so far (role,
database, tablespace) are on global, schemaless objects, so the question never
came up -- pg_get_policy_ddl is the first of the family whose output embeds
schema-qualifiable references. Your own pg_get_table_ddl patch already hit
this and deparses under a controlled search_path (narrowed to pg_catalog), so
the most consistent fix is to do the same here (NewGUCNestLevel + set_config);
an empty search_path already yields the fully-qualified s1.f(a). If instead
the intent is to follow the caller's search_path, that's fine too, but it
should be documented (and SET search_path = '' noted as the way to get
portable DDL).

2) The doc calls the second parameter policy_name, but the actual argument is
policyname, so the documented named-argument call fails:

    SELECT * FROM pg_get_policy_ddl("table" => 't'::regclass,
policy_name => 'p_all');
    ERROR:  function pg_get_policy_ddl(table => regclass, policy_name
=> unknown) does not exist

Regards,
Rui






^ permalink  raw  reply  [nested|flat] 44+ messages in thread

* Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
@ 2026-07-09 10:57  Akshay Joshi <[email protected]>
  parent: Rui Zhao <[email protected]>
  0 siblings, 0 replies; 44+ messages in thread

From: Akshay Joshi @ 2026-07-09 10:57 UTC (permalink / raw)
  To: Rui Zhao <[email protected]>; +Cc: solai v <[email protected]>; Ilmar Y <[email protected]>; [email protected]

Thanks Rui for the review. I have tried to fix the issues you raised.
The v16 patch is ready for review/commit.

On Wed, Jul 8, 2026 at 10:28 PM Rui Zhao <[email protected]> wrote:

> Hi Akshay,
>
> I tested v15 on current master (57f93af36f): builds, make check passes, and
> basic reconstruction, pretty mode, NULL handling, and default-clause
> omission
> all look good. Two things.
>
> 1) Back in the v3/v4 discussion you decided the ON <table> name should
> always
> be schema-qualified for safety (the pg_get_triggerdef_worker thread with
> jian he), and that pretty here only controls formatting, not schema (Phil's
> point). That reasoning didn't reach the USING / WITH CHECK expressions,
> though: object references inside them are still qualified only by the
> caller's
> search_path, so they can lose their schema and rebind to a different object
> when the DDL is replayed elsewhere:
>
>     CREATE FUNCTION s1.f(int) RETURNS bool LANGUAGE sql AS 'SELECT $1 > 0';
>     CREATE FUNCTION s2.f(int) RETURNS bool LANGUAGE sql AS 'SELECT $1 < 0';
>     CREATE POLICY pf ON t2 USING (s1.f(a));
>
>     SET search_path = public, s1;
>     SELECT ddl FROM pg_get_policy_ddl('t2', 'pf') AS ddl;
>     --  CREATE POLICY pf ON public.t2 USING (f(a));      -- s1. dropped
>
>     SET search_path = public, s2;
>     CREATE POLICY pf ON t2 USING (f(a));                 -- now s2.f,
> opposite meaning
>
> So within one statement the ON clause is always qualified but the
> expression
> body isn't -- and as Phil noted, the pretty flag can't fix this, since
> pg_get_expr qualifies by search_path visibility regardless of pretty.
>
> Worth settling the contract, given where this function sits. The old
> pg_get_viewdef / ruledef / indexdef functions are search_path-aware and
> leave
> qualification to the caller (pg_dump sets search_path empty around them so
> the
> output is portable). The new pg_get_*_ddl functions committed so far (role,
> database, tablespace) are on global, schemaless objects, so the question
> never
> came up -- pg_get_policy_ddl is the first of the family whose output embeds
> schema-qualifiable references. Your own pg_get_table_ddl patch already hit
> this and deparses under a controlled search_path (narrowed to pg_catalog),
> so
> the most consistent fix is to do the same here (NewGUCNestLevel +
> set_config);
> an empty search_path already yields the fully-qualified s1.f(a). If instead
> the intent is to follow the caller's search_path, that's fine too, but it
> should be documented (and SET search_path = '' noted as the way to get
> portable DDL).
>
> 2) The doc calls the second parameter policy_name, but the actual argument
> is
> policyname, so the documented named-argument call fails:
>
>     SELECT * FROM pg_get_policy_ddl("table" => 't'::regclass,
> policy_name => 'p_all');
>     ERROR:  function pg_get_policy_ddl(table => regclass, policy_name
> => unknown) does not exist
>
> Regards,
> Rui
>


Attachments:

  [application/octet-stream] v16-0001-Add-pg_get_policy_ddl-to-reconstruct-CREATE.patch (34.0K, ../../CANxoLDd4uL=EU+4cGVWp1F9oqVaye4uybUkJhhS7QfeHaqeDXQ@mail.gmail.com/3-v16-0001-Add-pg_get_policy_ddl-to-reconstruct-CREATE.patch)
  download | inline diff:
From 4d82fcf9b04607804ecdcc4b3d90d57ac12ca671 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 22 May 2026 18:18:07 +0530
Subject: [PATCH v16] Add pg_get_policy_ddl() to reconstruct CREATE POLICY
 statements

  pg_get_policy_ddl(table regclass,
                    policyname name,
                    pretty bool DEFAULT false)
  RETURNS SETOF text

reconstructs the CREATE POLICY statement for the named row-level
security policy on the specified table.  Although a single statement is
produced, the function returns a SETOF text result so its calling
convention matches the rest of the pg_get_*_ddl family.

The reconstructed DDL includes all clauses of the CREATE POLICY syntax:
the policy's permissiveness (AS RESTRICTIVE), command type (FOR
SELECT/INSERT/UPDATE/DELETE), role list (TO <roles>), USING
qualification, and WITH CHECK expression.  Clauses whose value equals
the parser's default -- PERMISSIVE, FOR ALL, and TO PUBLIC -- are
omitted from the output, matching the convention used by pg_get_indexdef,
pg_get_constraintdef, pg_get_viewdef, and similar functions.  The result
is therefore semantically equivalent to the original DDL, not lexically
identical.

The pretty parameter controls output formatting and defaults to false,
following the same convention as pg_get_role_ddl, pg_get_tablespace_ddl,
and pg_get_database_ddl.  NULL passed explicitly for pretty is treated
as false.

NULL inputs for the table or policy name yield no rows.  An invalid
relation name surfaces the regclass resolution error; a non-existent
policy raises an explicit "policy ... does not exist" error.

Usage examples:

  -- compact form (default)
  SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1');
  SELECT * FROM pg_get_policy_ddl(16564, 'pol1');

  -- pretty-printed form
  SELECT * FROM pg_get_policy_ddl('rls_table', 'pol1', true);

Regression coverage is added to src/test/regress/sql/rowsecurity.sql
and exercises all valid combinations of the CREATE POLICY syntax:
PERMISSIVE/RESTRICTIVE, all FOR command variants (ALL/SELECT/INSERT/
UPDATE/DELETE), multi-role TO lists, USING-only, WITH CHECK-only, and
USING+WITH CHECK policies for both ALL and UPDATE commands (the only
two that accept both), RESTRICTIVE on a specific command type,
subquery expressions, pretty and non-pretty output, all boolean
representations for the pretty argument (true/false, on/off, 1/0),
NULL and error paths, and a round-trip test that drops and re-executes
the generated DDL.

Author: Akshay Joshi <[email protected]>
Reviewed-by: Rui Zhao <[email protected]>
---
 doc/src/sgml/func/func-info.sgml          |  19 ++
 src/backend/utils/adt/ddlutils.c          | 274 +++++++++++++++++++
 src/include/catalog/pg_proc.dat           |   6 +
 src/test/regress/expected/rowsecurity.out | 315 ++++++++++++++++++++++
 src/test/regress/sql/rowsecurity.sql      | 150 +++++++++++
 5 files changed, 764 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 69ef3857cfa..cfdef22ad5a 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3960,6 +3960,25 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
         is false, the <literal>OWNER</literal> clause is omitted.
        </para></entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_policy_ddl</primary>
+        </indexterm>
+        <function>pg_get_policy_ddl</function>
+        ( <parameter>table</parameter> <type>regclass</type>,
+        <parameter>policyname</parameter> <type>name</type>
+        <optional>, <parameter>pretty</parameter> <type>boolean</type>
+        </optional> )
+        <returnvalue>setof text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE POLICY</command> statement for the
+        named row-level security policy on the specified table.  The result
+        is returned as a single row.  If <parameter>pretty</parameter> is
+        true, the output is formatted for readability; the default is false.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c
index a70f1c28655..2ada0e25fde 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
+#include "catalog/pg_policy.h"
 #include "catalog/pg_tablespace.h"
 #include "commands/tablespace.h"
 #include "common/relpath.h"
@@ -56,6 +57,9 @@ static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner
 static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid);
 static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
 										  bool no_owner, bool no_tablespace);
+static List *pg_get_policy_ddl_internal(Oid tableID, const char *policyName,
+										bool pretty);
+static const char *get_policy_cmd_name(char cmd);
 
 
 /*
@@ -975,3 +979,273 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
 		SRF_RETURN_DONE(funcctx);
 	}
 }
+
+/*
+ * get_policy_cmd_name
+ *		Map a non-ALL pg_policy.polcmd char to its SQL keyword.
+ *
+ * Callers must guard against '*' (ALL) themselves; it is not passed here.
+ */
+static const char *
+get_policy_cmd_name(char cmd)
+{
+	switch (cmd)
+	{
+		case ACL_SELECT_CHR:
+			return "SELECT";
+		case ACL_INSERT_CHR:
+			return "INSERT";
+		case ACL_UPDATE_CHR:
+			return "UPDATE";
+		case ACL_DELETE_CHR:
+			return "DELETE";
+		default:
+			elog(ERROR, "unrecognized policy command: %d", (int) cmd);
+			return NULL;		/* keep compiler quiet */
+	}
+}
+
+/*
+ * pg_get_policy_ddl_internal
+ *		Generate the DDL statement to recreate a row-level security policy.
+ *
+ * Returns a List containing a single palloc'd string with the CREATE POLICY
+ * statement.  Returning a List keeps the calling convention consistent with
+ * the rest of the pg_get_*_ddl family even though only one row is produced.
+ */
+static List *
+pg_get_policy_ddl_internal(Oid tableID, const char *policyName, bool pretty)
+{
+	Relation	pgPolicyRel;
+	HeapTuple	tuplePolicy;
+	Form_pg_policy policyForm;
+	ScanKeyData skey[2];
+	SysScanDesc sscan;
+	StringInfoData buf;
+	Datum		valueDatum;
+	bool		attrIsNull;
+	char	   *relname;
+	char	   *nspname;
+	char	   *targetTable;
+	int			save_nestlevel;
+	List	   *statements = NIL;
+
+	/* Validate that the relation exists and build its qualified name. */
+	relname = get_rel_name(tableID);
+	if (relname == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_TABLE),
+				 errmsg("relation with OID %u does not exist", tableID)));
+
+	nspname = get_namespace_name(get_rel_namespace(tableID));
+	if (nspname == NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_SCHEMA),
+				 errmsg("schema for relation with OID %u does not exist",
+						tableID)));
+
+	targetTable = quote_qualified_identifier(nspname, relname);
+	pfree(relname);
+	pfree(nspname);
+
+	pgPolicyRel = table_open(PolicyRelationId, AccessShareLock);
+
+	/* Set key - policy's relation id. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(tableID));
+
+	/* Set key - policy's name. */
+	ScanKeyInit(&skey[1],
+				Anum_pg_policy_polname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(policyName));
+
+	sscan = systable_beginscan(pgPolicyRel,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 2,
+							   skey);
+
+	tuplePolicy = systable_getnext(sscan);
+	if (!HeapTupleIsValid(tuplePolicy))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("policy \"%s\" for table \"%s\" does not exist",
+						policyName, targetTable)));
+
+	policyForm = (Form_pg_policy) GETSTRUCT(tuplePolicy);
+
+	initStringInfo(&buf);
+
+	/* Build the CREATE POLICY statement */
+	appendStringInfo(&buf, "CREATE POLICY %s ON %s",
+					 quote_identifier(policyName),
+					 targetTable);
+	pfree(targetTable);
+
+	/*
+	 * Emit AS RESTRICTIVE only when it differs from the default (PERMISSIVE).
+	 */
+	if (!policyForm->polpermissive)
+		append_ddl_option(&buf, pretty, 4, "AS RESTRICTIVE");
+
+	/*
+	 * Emit FOR <cmd> only when it differs from the default (ALL, encoded as
+	 * '*').
+	 */
+	if (policyForm->polcmd != '*')
+		append_ddl_option(&buf, pretty, 4, "FOR %s",
+						  get_policy_cmd_name(policyForm->polcmd));
+
+	/*
+	 * Emit TO <roles> only when it differs from the default (PUBLIC).  PUBLIC
+	 * is encoded in polroles as a single InvalidOid element, so we omit the
+	 * clause whenever every entry is InvalidOid.  polroles is always non-NULL.
+	 */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polroles,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	Assert(!attrIsNull);
+	{
+		ArrayType  *policy_roles = DatumGetArrayTypeP(valueDatum);
+		int			nitems = ARR_DIMS(policy_roles)[0];
+		Oid		   *roles = (Oid *) ARR_DATA_PTR(policy_roles);
+		StringInfoData role_names;
+
+		initStringInfo(&role_names);
+
+		for (int i = 0; i < nitems; i++)
+		{
+			if (OidIsValid(roles[i]))
+			{
+				char	   *rolename = GetUserNameFromId(roles[i], false);
+
+				if (role_names.len > 0)
+					appendStringInfoString(&role_names, ", ");
+				appendStringInfoString(&role_names, quote_identifier(rolename));
+				pfree(rolename);
+			}
+		}
+
+		if (role_names.len > 0)
+			append_ddl_option(&buf, pretty, 4, "TO %s", role_names.data);
+
+		pfree(role_names.data);
+	}
+
+	/*
+	 * Deparse USING and WITH CHECK expressions under an empty search_path so
+	 * that all object references are fully schema-qualified.  This makes the
+	 * reconstructed DDL portable regardless of the caller's search_path
+	 * setting, consistent with how pg_dump handles pg_get_viewdef and similar
+	 * functions.
+	 */
+	save_nestlevel = NewGUCNestLevel();
+	(void) set_config_option("search_path", "",
+							 PGC_USERSET, PGC_S_SESSION,
+							 GUC_ACTION_SAVE, true, 0, false);
+
+	/* USING expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polqual,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "USING (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	/* WITH CHECK expression */
+	valueDatum = heap_getattr(tuplePolicy,
+							  Anum_pg_policy_polwithcheck,
+							  RelationGetDescr(pgPolicyRel),
+							  &attrIsNull);
+	if (!attrIsNull)
+	{
+		Datum		expr;
+
+		expr = DirectFunctionCall3(pg_get_expr_ext,
+								   valueDatum,
+								   ObjectIdGetDatum(policyForm->polrelid),
+								   BoolGetDatum(pretty));
+		append_ddl_option(&buf, pretty, 4, "WITH CHECK (%s)",
+						  TextDatumGetCString(expr));
+	}
+
+	AtEOXact_GUC(false, save_nestlevel);
+
+	appendStringInfoChar(&buf, ';');
+
+	statements = lappend(statements, pstrdup(buf.data));
+
+	systable_endscan(sscan);
+	table_close(pgPolicyRel, AccessShareLock);
+	pfree(buf.data);
+
+	return statements;
+}
+
+/*
+ * pg_get_policy_ddl
+ *		Return DDL to recreate a row-level security policy as a single text row.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	List	   *statements;
+
+	if (SRF_IS_FIRSTCALL())
+	{
+		MemoryContext oldcontext;
+		Oid			tableID;
+		Name		policyName;
+		bool		pretty;
+
+		funcctx = SRF_FIRSTCALL_INIT();
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+		{
+			MemoryContextSwitchTo(oldcontext);
+			SRF_RETURN_DONE(funcctx);
+		}
+
+		tableID = PG_GETARG_OID(0);
+		policyName = PG_GETARG_NAME(1);
+		pretty = !PG_ARGISNULL(2) && PG_GETARG_BOOL(2);
+
+		statements = pg_get_policy_ddl_internal(tableID,
+												NameStr(*policyName),
+												pretty);
+		funcctx->user_fctx = statements;
+		funcctx->max_calls = list_length(statements);
+
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	funcctx = SRF_PERCALL_SETUP();
+	statements = (List *) funcctx->user_fctx;
+
+	if (funcctx->call_cntr < funcctx->max_calls)
+	{
+		char	   *stmt;
+
+		stmt = list_nth(statements, funcctx->call_cntr);
+
+		SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+	}
+	else
+	{
+		list_free_deep(statements);
+		SRF_RETURN_DONE(funcctx);
+	}
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..cd029b483f1 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8634,6 +8634,12 @@
   proargtypes => 'regdatabase bool bool bool',
   proargnames => '{database,pretty,owner,tablespace}',
   proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' },
+{ oid => '9683', descr => 'get DDL to recreate a row-level security policy',
+  proname => 'pg_get_policy_ddl', prorows => '1', proisstrict => 'f',
+  proretset => 't', provolatile => 's', pronargdefaults => '1',
+  prorettype => 'text', proargtypes => 'regclass name bool',
+  proargnames => '{table,policyname,pretty}', proargdefaults => '{false}',
+  prosrc => 'pg_get_policy_ddl' },
 { oid => '2509',
   descr => 'deparse an encoded expression with pretty-print option',
   proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 3a5e82c35bd..da174eeadbf 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -5195,6 +5195,321 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 --
+-- Test for pg_get_policy_ddl(table, policy_name[, pretty]) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+-- Test UPDATE policy with both USING and WITH CHECK
+CREATE POLICY rls_p10 ON rls_tbl_1 FOR UPDATE
+    USING (dlevel > 0) WITH CHECK (dlevel < 100);
+-- Test ALL policy with both USING and WITH CHECK (AS PERMISSIVE and FOR ALL are defaults, omitted)
+CREATE POLICY rls_p11 ON rls_tbl_1
+    USING (dlevel >= 1) WITH CHECK (dlevel <= 99);
+-- Test RESTRICTIVE on a specific command
+CREATE POLICY rls_p12 ON rls_tbl_1 AS RESTRICTIVE FOR SELECT
+    USING (cid > 0);
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+ count 
+-------
+     0
+(1 row)
+
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+ count 
+-------
+     0
+(1 row)
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+ERROR:  relation "nonexistent_tbl" does not exist
+LINE 1: SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1')...
+                                        ^
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+ERROR:  policy "nonexistent_pol" for table "regress_rls_schema.rls_tbl_1" does not exist
+-- Without pretty formatting (default); also verify explicit false equals default
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+                                        pg_get_policy_ddl                                        
+-------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1 USING ((dlevel <= ( SELECT rls_tbl_2.seclv+
+    FROM regress_rls_schema.rls_tbl_2                                                           +
+   WHERE (rls_tbl_2.pguser = CURRENT_USER))));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE USING (((cid <> 44) AND (cid < 50)));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+                                   pg_get_policy_ddl                                    
+----------------------------------------------------------------------------------------
+ CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1 FOR SELECT USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+                                       pg_get_policy_ddl                                       
+-----------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1 FOR INSERT WITH CHECK (((cid % 2) = 1));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6');
+                                    pg_get_policy_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING (((cid % 2) = 0));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7');
+                                 pg_get_policy_ddl                                  
+------------------------------------------------------------------------------------
+ CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1 FOR DELETE USING ((cid < 8));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8');
+                                             pg_get_policy_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9');
+                                                      pg_get_policy_ddl                                                      
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1 TO regress_rls_exempt_user WITH CHECK ((cid = ( SELECT rls_tbl_2.seclv+
+    FROM regress_rls_schema.rls_tbl_2)));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10');
+                                                 pg_get_policy_ddl                                                  
+--------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p10 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING ((dlevel > 0)) WITH CHECK ((dlevel < 100));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p11');
+                                            pg_get_policy_ddl                                             
+----------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p11 ON regress_rls_schema.rls_tbl_1 USING ((dlevel >= 1)) WITH CHECK ((dlevel <= 99));
+(1 row)
+
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p12');
+                                         pg_get_policy_ddl                                          
+----------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p12 ON regress_rls_schema.rls_tbl_1 AS RESTRICTIVE FOR SELECT USING ((cid > 0));
+(1 row)
+
+-- Explicit false must match omitting the argument
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', false);
+                                                 pg_get_policy_ddl                                                  
+--------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p10 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING ((dlevel > 0)) WITH CHECK ((dlevel < 100));
+(1 row)
+
+-- NULL pretty must also default to non-pretty
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', NULL);
+                                                 pg_get_policy_ddl                                                  
+--------------------------------------------------------------------------------------------------------------------
+ CREATE POLICY rls_p10 ON regress_rls_schema.rls_tbl_1 FOR UPDATE USING ((dlevel > 0)) WITH CHECK ((dlevel < 100));
+(1 row)
+
+-- pretty accepts all standard boolean representations: true/false, on/off, 1/0
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'on'::bool);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', '1'::bool);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'off'::bool);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', '0'::bool);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1 USING ((dauthor = CURRENT_USER));
+(1 row)
+\pset format aligned
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p1 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel <= (( SELECT rls_tbl_2.seclv
+   FROM regress_rls_schema.rls_tbl_2
+  WHERE rls_tbl_2.pguser = CURRENT_USER)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p2 ON regress_rls_schema.rls_tbl_1
+    AS RESTRICTIVE
+    USING (cid <> 44 AND cid < 50);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p3 ON regress_rls_schema.rls_tbl_1
+    USING (dauthor = CURRENT_USER);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p4 ON regress_rls_schema.rls_tbl_1
+    FOR SELECT
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p5 ON regress_rls_schema.rls_tbl_1
+    FOR INSERT
+    WITH CHECK ((cid % 2) = 1);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p6 ON regress_rls_schema.rls_tbl_1
+    FOR UPDATE
+    USING ((cid % 2) = 0);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p7 ON regress_rls_schema.rls_tbl_1
+    FOR DELETE
+    USING (cid < 8);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p8 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_dave, regress_rls_alice
+    USING (true);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p9 ON regress_rls_schema.rls_tbl_1
+    TO regress_rls_exempt_user
+    WITH CHECK (cid = (( SELECT rls_tbl_2.seclv
+   FROM regress_rls_schema.rls_tbl_2)));
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p10 ON regress_rls_schema.rls_tbl_1
+    FOR UPDATE
+    USING (dlevel > 0)
+    WITH CHECK (dlevel < 100);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p11', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p11 ON regress_rls_schema.rls_tbl_1
+    USING (dlevel >= 1)
+    WITH CHECK (dlevel <= 99);
+(1 row)
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p12', true);
+pg_get_policy_ddl
+CREATE POLICY rls_p12 ON regress_rls_schema.rls_tbl_1
+    AS RESTRICTIVE
+    FOR SELECT
+    USING (cid > 0);
+(1 row)
+\pset format aligned
+-- Round-trip: the generated DDL must be re-executable, including for atomic
+-- boolean expressions that pg_get_expr() does not parenthesize.
+CREATE TABLE rls_rt (a int);
+CREATE POLICY rt_true ON rls_rt USING (true);
+CREATE POLICY rt_false ON rls_rt FOR INSERT WITH CHECK (false);
+CREATE TEMP TABLE rt_ddl AS
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_true') AS ddl
+    UNION ALL
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_false');
+DROP POLICY rt_true ON rls_rt;
+DROP POLICY rt_false ON rls_rt;
+SELECT ddl FROM rt_ddl ORDER BY ddl \gexec
+CREATE POLICY rt_false ON regress_rls_schema.rls_rt FOR INSERT WITH CHECK (false);
+CREATE POLICY rt_true ON regress_rls_schema.rls_rt USING (true);
+SELECT polname FROM pg_policy WHERE polrelid = 'rls_rt'::regclass ORDER BY polname;
+ polname  
+----------
+ rt_false
+ rt_true
+(2 rows)
+
+DROP TABLE rls_rt;
+-- Schema-qualification: function references in expressions must be
+-- fully qualified regardless of the caller's search_path.
+CREATE SCHEMA rls_s1;
+CREATE SCHEMA rls_s2;
+CREATE FUNCTION rls_s1.rls_f(int) RETURNS bool LANGUAGE sql AS 'SELECT $1 > 0';
+CREATE FUNCTION rls_s2.rls_f(int) RETURNS bool LANGUAGE sql AS 'SELECT $1 < 0';
+CREATE TABLE rls_tbl_3 (a int);
+CREATE POLICY rls_pf ON rls_tbl_3 USING (rls_s1.rls_f(a));
+-- With rls_s1 in path, rls_f should still appear schema-qualified in DDL.
+SET search_path = regress_rls_schema, rls_s1;
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_3', 'rls_pf');
+pg_get_policy_ddl
+CREATE POLICY rls_pf ON regress_rls_schema.rls_tbl_3 USING (rls_s1.rls_f(a));
+(1 row)
+\pset format aligned
+-- Restore the test's search_path before cleanup.
+SET search_path = regress_rls_schema;
+DROP POLICY rls_pf ON rls_tbl_3;
+DROP TABLE rls_tbl_3;
+DROP FUNCTION rls_s1.rls_f(int);
+DROP FUNCTION rls_s2.rls_f(int);
+DROP SCHEMA rls_s1;
+DROP SCHEMA rls_s2;
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP POLICY rls_p10 ON rls_tbl_1;
+DROP POLICY rls_p11 ON rls_tbl_1;
+DROP POLICY rls_p12 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+--
 -- Clean up objects
 --
 RESET SESSION AUTHORIZATION;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 6b3566271df..cdc50d74cde 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -2598,6 +2598,156 @@ reset rls_test.blah;
 drop function rls_f(text);
 drop table rls_t, test_t;
 
+--
+-- Test for pg_get_policy_ddl(table, policy_name[, pretty]) function.
+--
+CREATE TABLE rls_tbl_1 (
+    did         int primary key,
+    cid         int,
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON rls_tbl_1 TO public;
+CREATE TABLE rls_tbl_2 (
+    pguser      name primary key,
+    seclv       int
+);
+GRANT SELECT ON rls_tbl_2 TO public;
+
+-- Test PERMISSIVE and RESTRICTIVE
+CREATE POLICY rls_p1 ON rls_tbl_1 AS PERMISSIVE
+    USING (dlevel <= (SELECT seclv FROM rls_tbl_2 WHERE pguser = current_user));
+CREATE POLICY rls_p2 ON rls_tbl_1 AS RESTRICTIVE USING (cid <> 44 AND cid < 50);
+
+-- Test FOR ALL | SELECT | INSERT | UPDATE | DELETE
+CREATE POLICY rls_p3 ON rls_tbl_1 FOR ALL USING (dauthor = current_user);
+CREATE POLICY rls_p4 ON rls_tbl_1 FOR SELECT USING (cid % 2 = 0);
+CREATE POLICY rls_p5 ON rls_tbl_1 FOR INSERT WITH CHECK (cid % 2 = 1);
+CREATE POLICY rls_p6 ON rls_tbl_1 FOR UPDATE USING (cid % 2 = 0);
+CREATE POLICY rls_p7 ON rls_tbl_1 FOR DELETE USING (cid < 8);
+
+-- Test TO { role_name ... }
+CREATE POLICY rls_p8 ON rls_tbl_1 TO regress_rls_dave, regress_rls_alice USING (true);
+CREATE POLICY rls_p9 ON rls_tbl_1 TO regress_rls_exempt_user WITH CHECK (cid = (SELECT seclv FROM rls_tbl_2));
+
+-- Test UPDATE policy with both USING and WITH CHECK
+CREATE POLICY rls_p10 ON rls_tbl_1 FOR UPDATE
+    USING (dlevel > 0) WITH CHECK (dlevel < 100);
+
+-- Test ALL policy with both USING and WITH CHECK (AS PERMISSIVE and FOR ALL are defaults, omitted)
+CREATE POLICY rls_p11 ON rls_tbl_1
+    USING (dlevel >= 1) WITH CHECK (dlevel <= 99);
+
+-- Test RESTRICTIVE on a specific command
+CREATE POLICY rls_p12 ON rls_tbl_1 AS RESTRICTIVE FOR SELECT
+    USING (cid > 0);
+
+-- NULL inputs should return no rows
+SELECT count(*) FROM pg_get_policy_ddl(NULL, 'rls_p1');
+SELECT count(*) FROM pg_get_policy_ddl('rls_tbl_1', NULL);
+SELECT count(*) FROM pg_get_policy_ddl(NULL, NULL);
+
+-- Table does not exist
+SELECT * FROM pg_get_policy_ddl('nonexistent_tbl', 'rls_p1');
+-- Policy does not exist
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'nonexistent_pol');
+
+-- Without pretty formatting (default); also verify explicit false equals default
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p11');
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p12');
+-- Explicit false must match omitting the argument
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', false);
+-- NULL pretty must also default to non-pretty
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', NULL);
+
+-- pretty accepts all standard boolean representations: true/false, on/off, 1/0
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'on'::bool);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', '1'::bool);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', 'off'::bool);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', '0'::bool);
+\pset format aligned
+
+-- With pretty formatting
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p1', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p2', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p3', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p4', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p5', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p6', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p7', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p8', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p9', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p10', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p11', true);
+SELECT * FROM pg_get_policy_ddl('rls_tbl_1', 'rls_p12', true);
+\pset format aligned
+
+-- Round-trip: the generated DDL must be re-executable, including for atomic
+-- boolean expressions that pg_get_expr() does not parenthesize.
+CREATE TABLE rls_rt (a int);
+CREATE POLICY rt_true ON rls_rt USING (true);
+CREATE POLICY rt_false ON rls_rt FOR INSERT WITH CHECK (false);
+CREATE TEMP TABLE rt_ddl AS
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_true') AS ddl
+    UNION ALL
+    SELECT pg_get_policy_ddl('rls_rt', 'rt_false');
+DROP POLICY rt_true ON rls_rt;
+DROP POLICY rt_false ON rls_rt;
+SELECT ddl FROM rt_ddl ORDER BY ddl \gexec
+SELECT polname FROM pg_policy WHERE polrelid = 'rls_rt'::regclass ORDER BY polname;
+DROP TABLE rls_rt;
+
+-- Schema-qualification: function references in expressions must be
+-- fully qualified regardless of the caller's search_path.
+CREATE SCHEMA rls_s1;
+CREATE SCHEMA rls_s2;
+CREATE FUNCTION rls_s1.rls_f(int) RETURNS bool LANGUAGE sql AS 'SELECT $1 > 0';
+CREATE FUNCTION rls_s2.rls_f(int) RETURNS bool LANGUAGE sql AS 'SELECT $1 < 0';
+CREATE TABLE rls_tbl_3 (a int);
+CREATE POLICY rls_pf ON rls_tbl_3 USING (rls_s1.rls_f(a));
+-- With rls_s1 in path, rls_f should still appear schema-qualified in DDL.
+SET search_path = regress_rls_schema, rls_s1;
+\pset format unaligned
+SELECT * FROM pg_get_policy_ddl('rls_tbl_3', 'rls_pf');
+\pset format aligned
+-- Restore the test's search_path before cleanup.
+SET search_path = regress_rls_schema;
+DROP POLICY rls_pf ON rls_tbl_3;
+DROP TABLE rls_tbl_3;
+DROP FUNCTION rls_s1.rls_f(int);
+DROP FUNCTION rls_s2.rls_f(int);
+DROP SCHEMA rls_s1;
+DROP SCHEMA rls_s2;
+
+-- Clean up objects created for testing pg_get_policy_ddl function.
+DROP POLICY rls_p1 ON rls_tbl_1;
+DROP POLICY rls_p2 ON rls_tbl_1;
+DROP POLICY rls_p3 ON rls_tbl_1;
+DROP POLICY rls_p4 ON rls_tbl_1;
+DROP POLICY rls_p5 ON rls_tbl_1;
+DROP POLICY rls_p6 ON rls_tbl_1;
+DROP POLICY rls_p7 ON rls_tbl_1;
+DROP POLICY rls_p8 ON rls_tbl_1;
+DROP POLICY rls_p9 ON rls_tbl_1;
+DROP POLICY rls_p10 ON rls_tbl_1;
+DROP POLICY rls_p11 ON rls_tbl_1;
+DROP POLICY rls_p12 ON rls_tbl_1;
+DROP TABLE rls_tbl_1;
+DROP TABLE rls_tbl_2;
+
 --
 -- Clean up objects
 --
-- 
2.51.0



^ permalink  raw  reply  [nested|flat] 44+ messages in thread


end of thread, other threads:[~2026-07-09 10:57 UTC | newest]

Thread overview: 44+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2018-10-12 12:53 [PATCH v3 1/2] Secondary index access optimizations Konstantin Knizhnik <[email protected]>
2025-10-15 13:37 [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-10-15 17:25 ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Álvaro Herrera <[email protected]>
2025-10-16 08:17   ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-10-27 16:45     ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Mark Wong <[email protected]>
2025-10-28 09:38       ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-11-03 11:47         ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-11-07 12:20           ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-11-07 13:14             ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Marcos Pegoraro <[email protected]>
2025-11-07 14:27               ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-11-07 14:47                 ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Marcos Pegoraro <[email protected]>
2025-11-10 05:16                   ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-11-20 09:27                     ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2026-01-05 14:30                       ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement jian he <[email protected]>
2026-05-22 13:32                         ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2026-05-22 16:24                           ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Japin Li <[email protected]>
2026-05-25 07:17                             ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2026-05-28 13:41                               ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Ilmar Y <[email protected]>
2026-05-29 06:50                                 ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2026-05-29 07:38                                   ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Japin Li <[email protected]>
2026-05-30 12:14                                     ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Ilmar Y <[email protected]>
2026-06-08 12:41                                     ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement solai v <[email protected]>
2026-06-22 13:04                                       ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2026-06-22 13:19                                         ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2026-06-29 13:42                                           ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2026-06-30 11:18                                             ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2026-07-01 09:15                                               ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement solai v <[email protected]>
2026-07-01 10:31                                                 ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2026-07-01 11:31                                                   ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement solai v <[email protected]>
2026-07-06 11:28                                                     ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2026-07-08 16:58                                                       ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Rui Zhao <[email protected]>
2026-07-09 10:57                                                         ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-10-15 17:30 ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Philip Alger <[email protected]>
2025-10-16 08:34   ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-10-16 09:14     ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement jian he <[email protected]>
2025-10-16 11:47       ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-10-16 12:36     ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Philip Alger <[email protected]>
2025-10-16 12:50       ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-10-21 09:08         ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Chao Li <[email protected]>
2025-10-22 13:19           ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-10-22 07:20         ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement jian he <[email protected]>
2025-10-22 13:21           ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>
2025-10-22 18:49             ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Philip Alger <[email protected]>
2025-10-25 05:57               ` Re: [PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement Akshay Joshi <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox