[PATCH] Add pg_get_policy_ddl() function to reconstruct CREATE POLICY statement
Hi Hackers,
I’m submitting a patch as part of the broader Retail DDL Functions project
described by Andrew Dunstan
/messages/by-id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9@dunslane.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:
0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patchapplication/octet-stream; name=0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patchDownload
From 7445dc629c2139905f73a6128d4ed30597bdecb1 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <akshay.joshi@enterprisedb.com>
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 akshay.joshi@enterprisedb.com
---
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
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/
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
On Wed, Oct 15, 2025 at 10:55 PM Álvaro Herrera <alvherre@kurilemu.de>
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 justprettyFlags = GET_PRETTY_FLAGS(pretty);
same as pg_get_querydef() does.
Fixed and added 'Reviewed-by:'
Show quoted text
Thanks
--
Álvaro Herrera PostgreSQL Developer —
https://www.EnterpriseDB.com/
On Wed, Oct 15, 2025 at 11:00 PM Philip Alger <paalger0@gmail.com> 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.
Show quoted text
However, I think you're going to get a lot of opinions on what
well-formatted SQL looks like.--
Best,
Phil Alger
Attachments:
v2-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patchapplication/octet-stream; name=v2-0001-Add-pg_get_policy_ddl-function-to-reconstruct-CREATE.patchDownload
From cfc9ce4918ca05e4558104177f7d6320364a4642 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <akshay.joshi@enterprisedb.com>
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 <akshay.joshi@enterprisedb.com>
Reviewed-by: Álvaro Herrera <alvherre@kurilemu.de>
---
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
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
/messages/by-id/20211219205422.GT17618@telsasoft.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.
On Thu, Oct 16, 2025 at 2:45 PM jian he <jian.universality@gmail.com> 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
/messages/by-id/20211219205422.GT17618@telsasoft.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]
Show quoted text
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:
Screenshot 2025-10-16 at 5.08.10 PM.pngimage/png; name="=?UTF-8?B?U2NyZWVuc2hvdCAyMDI1LTEwLTE2IGF0IDUuMDguMTA=?= =?UTF-8?B?4oCvUE0ucG5n?="Download
�PNG
IHDR � R w
; >iCCPICC Profile H��WXS��[R!� RBo��� RBh�w! J�� b/�
�]T����(v@���E��e],��7)���������������3��@�W,�A5 ��Kb��c�S�@d�� ����Y��� ������-����2�������� @�!N���r!> ^�K� �x���b�hK`�/�����4�'���eC� Y���d �v��^�P���I�� Pg@���;�q*�6�F�L����N��4��4���!������y����3����H}X���) �����N��0V��W����|�=�(5S���G
yyl�3����!�A���p%��.�@WZ(���C��"A^`��f�dr��Z�.a����D�W���4;����)�(�1����$��[#!V��1/;.Li3�(�9h#������8V
�W�c���X�}In��|���BN����Q�k�q����`W"V��� ol��\���@���n�(!N��A����S�9�J{�L�,�� v�+�S�����T������xE�xQ74Z��6 �5
LY@��[��=A�$ ����$��k(B$ yC����P ��C��� ������)�� ��{�|�h�["x�?�sa��xs`���{~���� �d������@b 1�D��
p��W?X�q&�18������6�c�MB;��$�<�OQF�v���E�������+��{Cu�����w�~X�/��
Y�2nYV?i�m?<
����R�Q�(6?�T�SsR�����(bM�7{��g�����m����"� v;�]��a����������'��5�-VO6�������e2��������/_P({G�d�4�0#3���_�#�9�`8;9� ��(^_ob��
D��;7� �O����`�;��G�s6L��P���TR��p�� ��p��c`l�|���~ ��(��D}&\�0� sA1(��P6��`'��zp���ep�����/@x>#BBh�GLK�qF����#�H2��d "D��@�#��J���T#��#�i�"���E:��5� �PUT5B���(e�ah<:�@��E�t)��Bw�u�i�2zmG_���T0]�s����R�tL���J�2�
���s���c��G���q� Wp����)�,| ^�����f�:�����4�!���I��2S ��2�v�a�Y�����D�.����b21�8�������x��F�$��H$}�=��E���I��������k�.��
���L"��E�y�2�.� �5�3�g����I���)�(�(�(��+�.�g�&���M��fQ�R�Qk�g��oTTT�T<TbT�*sT���S�����QUK�N��:^U��Tu��)���oh4����B��-�U����>����8j|��jjuj��^�S�-�Y�������_Q���hXi�5��4*4�h�����k������\��K��f�I�J+P���@k���N:F7���<�|�6�Yz�6Q�Z����]��G�U�OGK�E'Q�P�B��N�.�k�����]�{@����aF�X���vm�{��z~z���z7�>�3����W���?4�
�b�l48k�;\{��p������3D
�c
�n5l1�726
6�7:c�k�k�g�e����q� ���Dh�����s����a�c43�L
MCL��[L[M?�Y�%��3�k���j�4O7_m�d�gaba1�����%��i�i�����{+k�$��V�V��z��"��64_�)6U67l��L�l�
�W�P;W�L�
�+������~�}����U#n;�:�
j:u��9�;�i12e����G~sru�q��t����Q�F5�z�l��s�p�1�6:h���
�_���\6��q��F�.tmr����&q�u�q�pOu�t���fF3�0/x<�=f{�������y��//�l�]^�c���l��m���������I�����k�����}�g�������e��b�f��w������d�d�
��JZ���e���O>B Yr�c��q�9}���3C��T��������K�#����U"-#E��Q ��*�a�u����1���������bg�����M���.�?~Y���iBS�z������II+����;s��d�darC
)%1e{J���qk�u�w_<��� �.N4��3��$�I�IS �I��R�p��U��4NZeZ��[�{�������+����W�wgxg������,������WY!Y���gGe���I���K�M�="�e��'O.��&����xNY3�O&����M�k���?�-R�/������S�,�,�L���x���������y��f���;�c&k��Y���YM��g/��5'x������s��4o������7.0Z0gA�/����K�o/�Z�i�H��u����+��\*u*-+��������~]����������m\N\.Z~k����+5W��\��n5cu���k&��X�R�i-u�tm���u
�-�/_��<��f���J�����7�7\����v����M�67��������l+qk���������[�v�������h�������z���e5h���g���W��i�u���Wwo�>�O��������h:�<X{��P�a���:�nZ]_}f}{CrC���#M�^���:�q��X�q���NPO,81p��d�)�����;�&5�?3��������ag/�:w�<�����]��x��R�e��u-�-�w��p�[k��+
W=�6��i;q�����������|3�f���[wn���~���n��W�
�}�?��A�C��e�U�a���v���-�����u�x���K�����e�L�Uw;w� ���|�����{���������C����7��������%o���x����?�����w���|����#���OI��}����e�W������=�s%\�� +���� �����u���'/���*G�?a�Q^� ����1����6 �����W@4
�x��=T�j�s���9`s����4�o����C�?�@��~n���|T��� �eXIfMM * > F( �i N � � �� x� �� R ASCII Screenshot~�)� pHYs % %IR$� �iTXtXML:com.adobe.xmp <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:exif="http://ns.adobe.com/exif/1.0/">
<exif:PixelYDimension>594</exif:PixelYDimension>
<exif:PixelXDimension>1964</exif:PixelXDimension>
<exif:UserComment>Screenshot</exif:UserComment>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
��+� iDOT ) ( ) ) ��� >