Attached is a patch adding two new functions for generating DDL to
recreate roles: pg_get_role_ddl() and pg_get_role_ddl_statements().

These functions return the CREATE ROLE statement and any ALTER ROLE SET
configuration parameters needed to recreate a role.  The former returns
everything as a single text string, while the latter returns each
statement as a separate row for easier programmatic processing.

The main use case is dumping role definitions for migration or backup
purposes without needing pg_dumpall.  The functions handle all role
attributes (LOGIN, SUPERUSER, etc.) and both role-wide and
database-specific configuration parameters.

We intentionally don't include passwords, since we can only see the
hashed values.  System roles (names starting with "pg_") are rejected
with an error, as users shouldn't be recreating those anyway.

To test:

  CREATE ROLE testrole LOGIN CREATEDB CONNECTION LIMIT 5;
  ALTER ROLE testrole SET work_mem TO '64MB';
  SELECT pg_get_role_ddl('testrole');

Should produce:

CREATE ROLE testrole LOGIN NOSUPERUSER CREATEDB NOCREATEROLE INHERIT NOREPLICATION NOBYPASSRLS CONNECTION LIMIT 5;
  ALTER ROLE testrole SET work_mem TO '64MB';

The patch includes regression tests covering various role configurations.

Co-authored-by: Mario Gonzalez and Bryan Green.

Comments?

BG
From 8d9e7d59ea216ae5f008c925313ee16240820377 Mon Sep 17 00:00:00 2001
From: Bryan Green <[email protected]>
Date: Fri, 24 Oct 2025 11:29:50 -0500
Subject: [PATCH] Add functions to generate DDL for recreating roles.

pg_get_role_ddl(regrole) returns the DDL needed to recreate a role
as a single text string.  pg_get_role_ddl_statements(regrole) returns
the same thing as a set of rows, one per DDL statement.

The output includes the CREATE ROLE statement with all role attributes,
plus any ALTER ROLE SET configuration parameters (both role-wide and
database-specific settings).

Passwords cannot be included, since we can only see the hashed values.
System roles (names starting with "pg_") are rejected, since users
shouldn't be recreating those anyway.

Co-authored-by: Mario Gonzalez <[email protected]>
Co-authored-by: Bryan Green <[email protected]>
---
 doc/src/sgml/func/func-info.sgml       |  56 ++++-
 src/backend/utils/adt/ruleutils.c      | 324 +++++++++++++++++++++++++
 src/include/catalog/pg_proc.dat        |  24 ++
 src/test/regress/expected/role_ddl.out |  90 +++++++
 src/test/regress/parallel_schedule     |   4 +
 src/test/regress/sql/role_ddl.sql      |  57 +++++
 6 files changed, 554 insertions(+), 1 deletion(-)
 create mode 100644 src/test/regress/expected/role_ddl.out
 create mode 100644 src/test/regress/sql/role_ddl.sql

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index c393832d94..b98c2dae22 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -2174,7 +2174,61 @@ SELECT currval(pg_get_serial_sequence('sometable', 
'id'));
      </tbody>
     </tgroup>
    </table>
-
+  <table id="functions-object-ddl-table">
+    <title>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_role_ddl</primary>
+         </indexterm>
+         <function>pg_get_role_ddl</function> ( <parameter>role</parameter> 
<type>regrole</type> )
+         <returnvalue>text</returnvalue>
+        </para>
+        <para>
+         Returns the DDL commands that would recreate the given role as a 
single text string.
+         The result includes the <command>CREATE ROLE</command> statement and 
any
+         <command>ALTER ROLE</command> statements needed to set role 
configuration parameters.
+         Password information is never included in the output.
+        </para>
+        <para>
+         Returns <literal>NULL</literal> if the role does not exist.
+        </para>
+       </entry>
+      </row>
+      <row>
+       <entry role="func_table_entry">
+        <para role="func_signature">
+         <indexterm>
+          <primary>pg_get_role_ddl_statements</primary>
+         </indexterm>
+         <function>pg_get_role_ddl_statements</function> ( 
<parameter>role</parameter> <type>regrole</type> )
+         <returnvalue>setof text</returnvalue>
+        </para>
+        <para>
+         Returns the DDL commands that would recreate the given role as a set 
of rows,
+         with each statement returned as a separate row. The first row 
contains the
+         <command>CREATE ROLE</command> statement, followed by any 
<command>ALTER ROLE</command>
+         statements needed to set role configuration parameters. This format 
is useful for
+         programmatic processing or when you want to filter or analyze 
individual statements.
+        </para>
+        <para>
+         Returns an empty set if the role does not exist.
+        </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+  </table>
   <para>
    Most of the functions that reconstruct (decompile) database objects
    have an optional <parameter>pretty</parameter> flag, which
diff --git a/src/backend/utils/adt/ruleutils.c 
b/src/backend/utils/adt/ruleutils.c
index 79ec136231..6c46580a0e 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -28,8 +28,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_db_role_setting.h"
 #include "catalog/pg_depend.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_partitioned_table.h"
@@ -59,6 +60,7 @@
 #include "rewrite/rewriteSupport.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/datetime.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
 #include "utils/hsearch.h"
@@ -549,6 +551,328 @@ static void get_json_table_nested_columns(TableFunc *tf, 
JsonTablePlan *plan,
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
+/*
+ * pg_get_role_ddl_internal
+ *             Generate DDL statements to recreate a role
+ *
+ * Returns a List of palloc'd strings, each being a complete SQL statement.
+ * The first list element is always the CREATE ROLE statement; subsequent
+ * elements are ALTER ROLE SET statements for any role-specific or
+ * role-in-database configuration settings.
+ *
+ * Returns NIL if the role OID is invalid.  This can happen if the role was
+ * dropped concurrently, or if we're passed a OID that doesn't match
+ * any role.
+ */
+static List *
+pg_get_role_ddl_internal(Oid roleid)
+{
+       HeapTuple       tuple;
+       Form_pg_authid roleform;
+       StringInfoData buf;
+       char       *rolname;
+       Datum           rolevaliduntil;
+       bool            isnull;
+       Relation        rel;
+       ScanKeyData scankey;
+       SysScanDesc scan;
+       HeapTuple       setting_tuple;
+       List       *statements = NIL;
+       const char *separator = " ";
+
+       tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+       if (!HeapTupleIsValid(tuple))
+               return NIL;
+
+       roleform = (Form_pg_authid) GETSTRUCT(tuple);
+       rolname = NameStr(roleform->rolname);
+
+       /*
+        * We don't support generating DDL for system roles.  The primary reason
+        * for this is that users shouldn't be recreating them.
+        */
+       if (strncmp(rolname, "pg_", 3) == 0)
+               ereport(ERROR,
+                               (errcode(ERRCODE_RESERVED_NAME),
+                                errmsg("role name \"%s\" is reserved", 
rolname),
+                                errdetail("Role names starting with \"pg_\" 
are reserved for system roles.")));
+
+       initStringInfo(&buf);
+       appendStringInfo(&buf, "CREATE ROLE %s", quote_identifier(rolname));
+
+       /*
+        * Append role attributes.  The order here follows the same sequence as
+        * you'd typically write them in a CREATE ROLE command, though any order
+        * is actually acceptable to the parser.
+        */
+       appendStringInfo(&buf, "%s%s", separator,
+                                        roleform->rolcanlogin ? "LOGIN" : 
"NOLOGIN");
+
+       appendStringInfo(&buf, "%s%s", separator,
+                                        roleform->rolsuper ? "SUPERUSER" : 
"NOSUPERUSER");
+
+       appendStringInfo(&buf, "%s%s", separator,
+                                        roleform->rolcreatedb ? "CREATEDB" : 
"NOCREATEDB");
+
+       appendStringInfo(&buf, "%s%s", separator,
+                                        roleform->rolcreaterole ? "CREATEROLE" 
: "NOCREATEROLE");
+
+       appendStringInfo(&buf, "%s%s", separator,
+                                        roleform->rolinherit ? "INHERIT" : 
"NOINHERIT");
+
+       appendStringInfo(&buf, "%s%s", separator,
+                                        roleform->rolreplication ? 
"REPLICATION" : "NOREPLICATION");
+
+       appendStringInfo(&buf, "%s%s", separator,
+                                        roleform->rolbypassrls ? "BYPASSRLS" : 
"NOBYPASSRLS");
+
+       /*
+        * CONNECTION LIMIT is only interesting if it's not -1 (the default,
+        * meaning no limit).
+        */
+       if (roleform->rolconnlimit >= 0)
+               appendStringInfo(&buf, "%sCONNECTION LIMIT %d",
+                                                separator, 
roleform->rolconnlimit);
+
+       rolevaliduntil = SysCacheGetAttr(AUTHOID, tuple,
+                                                                        
Anum_pg_authid_rolvaliduntil,
+                                                                        
&isnull);
+       if (!isnull)
+       {
+               struct pg_tm tm;
+               fsec_t          fsec;
+               char            ts_str[MAXDATELEN + 1];
+
+               if (timestamp2tm(rolevaliduntil, NULL, &tm, &fsec, NULL, NULL) 
== 0)
+               {
+                       EncodeDateTime(&tm, fsec, false, 0, "UTC", 
USE_ISO_DATES, ts_str);
+                       appendStringInfo(&buf, "%sVALID UNTIL %s",
+                                                        separator, 
quote_literal_cstr(ts_str));
+               }
+       }
+
+       /*
+        * We intentionally omit PASSWORD.  There's no way to retrieve the
+        * original password text from the stored hash, and even if we could,
+        * exposing passwords through a SQL function would be a security issue.
+        * Users must set passwords separately after recreating roles.
+        */
+
+       appendStringInfoChar(&buf, ';');
+
+       statements = lappend(statements, pstrdup(buf.data));
+
+       ReleaseSysCache(tuple);
+
+       /*
+        * Now scan pg_db_role_setting for ALTER ROLE SET configurations.
+        *
+        * These can be role-wide (setdatabase = 0) or specific to a particular
+        * database (setdatabase = a valid DB OID).  We generate one ALTER
+        * statement per setting, which isn't as compact as it could be, but is
+        * straightforward and matches how users typically set these up.
+        */
+       rel = table_open(DbRoleSettingRelationId, AccessShareLock);
+       ScanKeyInit(&scankey,
+                               Anum_pg_db_role_setting_setrole,
+                               BTEqualStrategyNumber, F_OIDEQ,
+                               ObjectIdGetDatum(roleid));
+       scan = systable_beginscan(rel, DbRoleSettingDatidRolidIndexId, true,
+                                                         NULL, 1, &scankey);
+
+       while (HeapTupleIsValid(setting_tuple = systable_getnext(scan)))
+       {
+               Form_pg_db_role_setting setting = (Form_pg_db_role_setting) 
GETSTRUCT(setting_tuple);
+               Oid                     datid = setting->setdatabase;
+               Datum           datum;
+               bool            setting_isnull;
+               ArrayType  *settings;
+               int                     i;
+               char       *datname = NULL;
+
+               /*
+                * The setconfig column is a text array in "name=value" format. 
It
+                * should never be null for a valid row, but be defensive.
+                */
+               datum = heap_getattr(setting_tuple, 
Anum_pg_db_role_setting_setconfig,
+                                                        RelationGetDescr(rel), 
&setting_isnull);
+               if (setting_isnull)
+                       continue;
+
+               settings = DatumGetArrayTypeP(datum);
+
+               /*
+                * If setdatabase is valid, this is a role-in-database setting;
+                * otherwise it's a role-wide setting.  Look up the database 
name once
+                * for all settings in this row.
+                */
+               if (OidIsValid(datid))
+               {
+                       datname = get_database_name(datid);
+                       if (datname == NULL)
+                       {
+                               /*
+                                * Database has been dropped; skip all settings 
in this row.
+                                */
+                               continue;
+                       }
+               }
+
+               /* Process each setting in the array */
+               for (i = 1; i <= ArrayGetNItems(ARR_NDIM(settings), 
ARR_DIMS(settings)); i++)
+               {
+                       Datum           setting_datum;
+                       bool            setting_elem_isnull;
+                       char       *setting_str;
+                       char       *equals_pos;
+
+                       setting_datum = array_ref(settings, 1, &i,
+                                                                         -1 /* 
varlenarray */ ,
+                                                                         -1 /* 
TEXT's typlen */ ,
+                                                                         false 
/* TEXT's typbyval */ ,
+                                                                         
TYPALIGN_INT /* TEXT's typalign */ ,
+                                                                         
&setting_elem_isnull);
+
+                       if (setting_elem_isnull)
+                               continue;
+
+                       setting_str = TextDatumGetCString(setting_datum);
+
+                       /*
+                        * Parse out the parameter name and value.  The format 
should
+                        * always be "name=value" but check anyway to avoid a 
crash if the
+                        * catalog is corrupted.
+                        */
+                       equals_pos = strchr(setting_str, '=');
+                       if (equals_pos == NULL)
+                       {
+                               pfree(setting_str);
+                               continue;
+                       }
+
+                       *equals_pos = '\0';
+
+                       /* Build a fresh ALTER ROLE statement for this setting 
*/
+                       resetStringInfo(&buf);
+                       appendStringInfo(&buf, "ALTER ROLE %s", 
quote_identifier(rolname));
+
+                       if (datname != NULL)
+                               appendStringInfo(&buf, " IN DATABASE %s",
+                                                                
quote_identifier(datname));
+
+                       appendStringInfo(&buf, " SET %s TO %s;",
+                                                        
quote_identifier(setting_str),
+                                                        
quote_literal_cstr(equals_pos + 1));
+
+                       statements = lappend(statements, pstrdup(buf.data));
+
+                       pfree(setting_str);
+               }
+
+               if (datname != NULL)
+                       pfree(datname);
+       }
+
+       systable_endscan(scan);
+       table_close(rel, AccessShareLock);
+
+       pfree(buf.data);
+
+       return statements;
+}
+
+
+/*
+ * pg_get_role_ddl
+ *             Return DDL to recreate a role as a single text string
+ *
+ * This is the main user-facing function.  It calls pg_get_role_ddl_internal
+ * to get the list of statements, then concatenates them with newlines.
+ *
+ * Returns NULL if the role OID doesn't exist.  This can only happen if
+ * you pass a OID rather than using the regrole type, or if there's
+ * a race condition with a concurrent DROP ROLE.
+ */
+Datum
+pg_get_role_ddl(PG_FUNCTION_ARGS)
+{
+       Oid                     roleid = PG_GETARG_OID(0);
+       List       *statements;
+       StringInfoData result;
+       ListCell   *lc;
+       bool            first = true;
+
+       statements = pg_get_role_ddl_internal(roleid);
+
+       if (statements == NIL)
+               PG_RETURN_NULL();
+
+       initStringInfo(&result);
+
+       foreach(lc, statements)
+       {
+               char       *stmt = (char *) lfirst(lc);
+
+               if (!first)
+                       appendStringInfoChar(&result, '\n');
+               appendStringInfoString(&result, stmt);
+               first = false;
+       }
+
+       list_free_deep(statements);
+
+       PG_RETURN_TEXT_P(cstring_to_text(result.data));
+}
+
+/*
+ * pg_get_role_ddl_statements
+ *             Return DDL to recreate a role as a set of rows
+ *
+ * This is similar to pg_get_role_ddl, but returns each statement as a
+ * separate row.  This is useful for programmatic processing or when you
+ * want to filter/analyze individual statements.
+ */
+Datum
+pg_get_role_ddl_statements(PG_FUNCTION_ARGS)
+{
+       FuncCallContext *funcctx;
+       List       *statements;
+       ListCell   *lc;
+
+       if (SRF_IS_FIRSTCALL())
+       {
+               MemoryContext oldcontext;
+               Oid                     roleid = PG_GETARG_OID(0);
+
+               funcctx = SRF_FIRSTCALL_INIT();
+               oldcontext = 
MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+               statements = pg_get_role_ddl_internal(roleid);
+               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;
+
+               lc = list_nth_cell(statements, funcctx->call_cntr);
+               stmt = (char *) lfirst(lc);
+
+               SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+       }
+       else
+       {
+               list_free_deep(statements);
+               SRF_RETURN_DONE(funcctx);
+       }
+}
+
 
 /* ----------
  * pg_get_ruledef              - Do it all and return a text
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index eecb43ec6f..0d0511589f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12604,4 +12604,28 @@
   proargnames => 
'{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
   prosrc => 'pg_get_aios' },
 
+# pg_get_role_ddl - return DDL to recreate a role (compact format)
+{ oid => '9991', descr => 'get SQL commands to recreate role',
+  proname => 'pg_get_role_ddl', pronamespace => 'pg_catalog',
+  proowner => 'POSTGRES', prolang => 'internal', procost => '1', prorows => 
'0',
+  provariadic => '0', prosupport => '0', prokind => 'f', prosecdef => 'f',
+  proleakproof => 'f', proisstrict => 't', proretset => 'f', provolatile => 
's',
+  proparallel => 's', pronargs => '1', pronargdefaults => '0',
+  prorettype => 'text', proargtypes => 'regrole', proallargtypes => '_null_',
+  proargmodes => '_null_', proargnames => '_null_', proargdefaults => '_null_',
+  protrftypes => '_null_', prosrc => 'pg_get_role_ddl', probin => '_null_',
+  prosqlbody => '_null_', proconfig => '_null_', proacl => '_null_' },
+
+# pg_get_role_ddl_statements - return DDL as separate statements (compact 
format)
+{ oid => '9992', descr => 'get SQL commands to recreate role as row set',
+  proname => 'pg_get_role_ddl_statements', pronamespace => 'pg_catalog',
+  proowner => 'POSTGRES', prolang => 'internal', procost => '1',
+  prorows => '10', provariadic => '0', prosupport => '0', prokind => 'f',
+  prosecdef => 'f', proleakproof => 'f', proisstrict => 't', proretset => 't',
+  provolatile => 's', proparallel => 's', pronargs => '1',
+  pronargdefaults => '0', prorettype => 'text', proargtypes => 'regrole',
+  proallargtypes => '_null_', proargmodes => '_null_', proargnames => '_null_',
+  proargdefaults => '_null_', protrftypes => '_null_',
+  prosrc => 'pg_get_role_ddl_statements', probin => '_null_',
+  prosqlbody => '_null_', proconfig => '_null_', proacl => '_null_' },
 ]
diff --git a/src/test/regress/expected/role_ddl.out 
b/src/test/regress/expected/role_ddl.out
new file mode 100644
index 0000000000..cbc4167a72
--- /dev/null
+++ b/src/test/regress/expected/role_ddl.out
@@ -0,0 +1,90 @@
+-- Set fixed timezone for consistent test results
+SET timezone = 'UTC';
+-- Create test database
+CREATE DATABASE regression_role_ddl_test;
+-- Test 1: Basic role
+CREATE ROLE regress_role_ddl_test1;
+SELECT pg_get_role_ddl('regress_role_ddl_test1');
+                                                  pg_get_role_ddl              
                                    
+-------------------------------------------------------------------------------------------------------------------
+ CREATE ROLE regress_role_ddl_test1 NOLOGIN NOSUPERUSER NOCREATEDB 
NOCREATEROLE INHERIT NOREPLICATION NOBYPASSRLS;
+(1 row)
+
+-- Test 2: Role with LOGIN
+CREATE ROLE regress_role_ddl_test2 LOGIN;
+SELECT pg_get_role_ddl('regress_role_ddl_test2');
+                                                 pg_get_role_ddl               
                                  
+-----------------------------------------------------------------------------------------------------------------
+ CREATE ROLE regress_role_ddl_test2 LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE 
INHERIT NOREPLICATION NOBYPASSRLS;
+(1 row)
+
+-- Test 3: Role with multiple privileges
+CREATE ROLE regress_role_ddl_test3
+  LOGIN
+  SUPERUSER
+  CREATEDB
+  CREATEROLE
+  CONNECTION LIMIT 5
+  VALID UNTIL '2030-12-31 23:59:59+00';
+SELECT pg_get_role_ddl('regress_role_ddl_test3');
+                                                                        
pg_get_role_ddl                                                                 
        
+----------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE ROLE regress_role_ddl_test3 LOGIN SUPERUSER CREATEDB CREATEROLE 
INHERIT NOREPLICATION NOBYPASSRLS CONNECTION LIMIT 5 VALID UNTIL '2030-12-31 
23:59:59';
+(1 row)
+
+-- Test 4: Role with configuration parameters
+CREATE ROLE regress_role_ddl_test4;
+ALTER ROLE regress_role_ddl_test4 SET work_mem TO '256MB';
+ALTER ROLE regress_role_ddl_test4 SET search_path TO 'myschema, public';
+SELECT pg_get_role_ddl('regress_role_ddl_test4');
+                                                  pg_get_role_ddl              
                                    
+-------------------------------------------------------------------------------------------------------------------
+ CREATE ROLE regress_role_ddl_test4 NOLOGIN NOSUPERUSER NOCREATEDB 
NOCREATEROLE INHERIT NOREPLICATION NOBYPASSRLS;+
+ ALTER ROLE regress_role_ddl_test4 SET work_mem TO '256MB';                    
                                   +
+ ALTER ROLE regress_role_ddl_test4 SET search_path TO '"myschema, public"';
+(1 row)
+
+-- Test 5: Role with database-specific configuration
+CREATE ROLE regress_role_ddl_test5;
+ALTER ROLE regress_role_ddl_test5 IN DATABASE regression_role_ddl_test SET 
work_mem TO '128MB';
+SELECT pg_get_role_ddl('regress_role_ddl_test5');
+                                                  pg_get_role_ddl              
                                    
+-------------------------------------------------------------------------------------------------------------------
+ CREATE ROLE regress_role_ddl_test5 NOLOGIN NOSUPERUSER NOCREATEDB 
NOCREATEROLE INHERIT NOREPLICATION NOBYPASSRLS;+
+ ALTER ROLE regress_role_ddl_test5 IN DATABASE regression_role_ddl_test SET 
work_mem TO '128MB';
+(1 row)
+
+-- Test 6: Test pg_get_role_ddl_statements function
+SELECT * FROM pg_get_role_ddl_statements('regress_role_ddl_test4');
+                                            pg_get_role_ddl_statements         
                                    
+-------------------------------------------------------------------------------------------------------------------
+ CREATE ROLE regress_role_ddl_test4 NOLOGIN NOSUPERUSER NOCREATEDB 
NOCREATEROLE INHERIT NOREPLICATION NOBYPASSRLS;
+ ALTER ROLE regress_role_ddl_test4 SET work_mem TO '256MB';
+ ALTER ROLE regress_role_ddl_test4 SET search_path TO '"myschema, public"';
+(3 rows)
+
+-- Test 7: Role with special characters (requires quoting)
+CREATE ROLE "regress_role-with-dash";
+SELECT pg_get_role_ddl('regress_role-with-dash');
+                                                   pg_get_role_ddl             
                                      
+---------------------------------------------------------------------------------------------------------------------
+ CREATE ROLE "regress_role-with-dash" NOLOGIN NOSUPERUSER NOCREATEDB 
NOCREATEROLE INHERIT NOREPLICATION NOBYPASSRLS;
+(1 row)
+
+-- Test 8: Non-existent role (should return NULL)
+SELECT pg_get_role_ddl(9999999::oid);
+ pg_get_role_ddl 
+-----------------
+ 
+(1 row)
+
+-- Cleanup
+DROP ROLE regress_role_ddl_test1;
+DROP ROLE regress_role_ddl_test2;
+DROP ROLE regress_role_ddl_test3;
+DROP ROLE regress_role_ddl_test4;
+DROP ROLE regress_role_ddl_test5;
+DROP ROLE "regress_role-with-dash";
+DROP DATABASE regression_role_ddl_test;
+-- Reset timezone to default
+RESET timezone;

diff --git a/src/test/regress/parallel_schedule 
b/src/test/regress/parallel_schedule
index a0f5fab0f5..34c9e98ce9 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -125,6 +125,10 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs 
prepare conversion tr
 # ----------
 test: partition_join partition_prune reloptions hash_part indexing 
partition_aggregate partition_info tuplesort explain compression 
compression_lz4 memoize stats predicate numa eager_aggregate
 
+# Tests role_ddl functions to create statements needed
+# to reproduce a role
+test: role_ddl
+
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
 # oidjoins is read-only, though, and should run late for best coverage
diff --git a/src/test/regress/sql/role_ddl.sql 
b/src/test/regress/sql/role_ddl.sql
new file mode 100644
index 0000000000..a27ff44137
--- /dev/null
+++ b/src/test/regress/sql/role_ddl.sql
@@ -0,0 +1,57 @@
+-- Set fixed timezone for consistent test results
+SET timezone = 'UTC';
+
+-- Create test database
+CREATE DATABASE regression_role_ddl_test;
+
+-- Test 1: Basic role
+CREATE ROLE regress_role_ddl_test1;
+SELECT pg_get_role_ddl('regress_role_ddl_test1');
+
+-- Test 2: Role with LOGIN
+CREATE ROLE regress_role_ddl_test2 LOGIN;
+SELECT pg_get_role_ddl('regress_role_ddl_test2');
+
+-- Test 3: Role with multiple privileges
+CREATE ROLE regress_role_ddl_test3
+  LOGIN
+  SUPERUSER
+  CREATEDB
+  CREATEROLE
+  CONNECTION LIMIT 5
+  VALID UNTIL '2030-12-31 23:59:59+00';
+SELECT pg_get_role_ddl('regress_role_ddl_test3');
+
+-- Test 4: Role with configuration parameters
+CREATE ROLE regress_role_ddl_test4;
+ALTER ROLE regress_role_ddl_test4 SET work_mem TO '256MB';
+ALTER ROLE regress_role_ddl_test4 SET search_path TO 'myschema, public';
+SELECT pg_get_role_ddl('regress_role_ddl_test4');
+
+-- Test 5: Role with database-specific configuration
+CREATE ROLE regress_role_ddl_test5;
+ALTER ROLE regress_role_ddl_test5 IN DATABASE regression_role_ddl_test SET 
work_mem TO '128MB';
+SELECT pg_get_role_ddl('regress_role_ddl_test5');
+
+-- Test 6: Test pg_get_role_ddl_statements function
+SELECT * FROM pg_get_role_ddl_statements('regress_role_ddl_test4');
+
+-- Test 7: Role with special characters (requires quoting)
+CREATE ROLE "regress_role-with-dash";
+SELECT pg_get_role_ddl('regress_role-with-dash');
+
+-- Test 8: Non-existent role (should return NULL)
+SELECT pg_get_role_ddl(9999999::oid);
+
+-- Cleanup
+DROP ROLE regress_role_ddl_test1;
+DROP ROLE regress_role_ddl_test2;
+DROP ROLE regress_role_ddl_test3;
+DROP ROLE regress_role_ddl_test4;
+DROP ROLE regress_role_ddl_test5;
+DROP ROLE "regress_role-with-dash";
+
+DROP DATABASE regression_role_ddl_test;
+
+-- Reset timezone to default
+RESET timezone;

-- 
2.46.0.windows.1

Reply via email to