From 2914a344cf60ec39cd42de92bf85fefdf5fb37c7 Mon Sep 17 00:00:00 2001
From: David Christensen <david.christensen@crunchydata.com>
Date: Wed, 10 Nov 2021 09:30:51 -0600
Subject: [PATCH] Support CREATE OR REPLACE for roles, users, and groups

---
 doc/src/sgml/ref/create_group.sgml           |  2 +-
 doc/src/sgml/ref/create_role.sgml            | 14 +++++-
 doc/src/sgml/ref/create_user.sgml            |  2 +-
 src/backend/commands/user.c                  | 26 +++++++++--
 src/backend/nodes/copyfuncs.c                |  1 +
 src/backend/nodes/equalfuncs.c               |  1 +
 src/backend/parser/gram.y                    | 30 ++++++++++++
 src/include/nodes/parsenodes.h               |  1 +
 src/test/regress/expected/roleattributes.out | 48 ++++++++++++++++++++
 src/test/regress/sql/roleattributes.sql      | 28 ++++++++++++
 10 files changed, 145 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/ref/create_group.sgml b/doc/src/sgml/ref/create_group.sgml
index d124c98eb5..da4061093f 100644
--- a/doc/src/sgml/ref/create_group.sgml
+++ b/doc/src/sgml/ref/create_group.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE GROUP <replaceable class="parameter">name</replaceable> [ [ WITH ] <replaceable class="parameter">option</replaceable> [ ... ] ]
+CREATE [OR REPLACE] GROUP <replaceable class="parameter">name</replaceable> [ [ WITH ] <replaceable class="parameter">option</replaceable> [ ... ] ]
 
 <phrase>where <replaceable class="parameter">option</replaceable> can be:</phrase>
 
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index b6a4ea1f72..d9d5443ac8 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE ROLE <replaceable class="parameter">name</replaceable> [ [ WITH ] <replaceable class="parameter">option</replaceable> [ ... ] ]
+CREATE [ OR REPLACE ] ROLE <replaceable class="parameter">name</replaceable> [ [ WITH ] <replaceable class="parameter">option</replaceable> [ ... ] ]
 
 <phrase>where <replaceable class="parameter">option</replaceable> can be:</phrase>
 
@@ -74,6 +74,18 @@ in sync when changing the above synopsis!
   <title>Parameters</title>
 
     <variablelist>
+     <varlistentry>
+      <term><literal>OR REPLACE</literal></term>
+      <listitem>
+       <para>
+        Ensure that the given role exists with the parameters provided on in
+        the command.  If the role already exists, this will force any
+        parameters on this role into the existing role.  This may end up with
+        some surprising behavior.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><replaceable class="parameter">name</replaceable></term>
       <listitem>
diff --git a/doc/src/sgml/ref/create_user.sgml b/doc/src/sgml/ref/create_user.sgml
index 48d2089238..e130a40cb9 100644
--- a/doc/src/sgml/ref/create_user.sgml
+++ b/doc/src/sgml/ref/create_user.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE USER <replaceable class="parameter">name</replaceable> [ [ WITH ] <replaceable class="parameter">option</replaceable> [ ... ] ]
+CREATE [ OR REPLACE ] USER <replaceable class="parameter">name</replaceable> [ [ WITH ] <replaceable class="parameter">option</replaceable> [ ... ] ]
 
 <phrase>where <replaceable class="parameter">option</replaceable> can be:</phrase>
 
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index aa69821be4..6fad8575bc 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -304,11 +304,27 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 	pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
 	pg_authid_dsc = RelationGetDescr(pg_authid_rel);
 
-	if (OidIsValid(get_role_oid(stmt->role, true)))
-		ereport(ERROR,
-				(errcode(ERRCODE_DUPLICATE_OBJECT),
-				 errmsg("role \"%s\" already exists",
-						stmt->role)));
+	if (OidIsValid(get_role_oid(stmt->role, true))) {
+		if (stmt->replace) {
+			AlterRoleStmt *alter = makeNode(AlterRoleStmt);
+
+			alter->role = makeNode(RoleSpec);
+			alter->role->roletype = ROLESPEC_CSTRING;
+			alter->role->rolename = stmt->role;
+			alter->role->location = -1;
+			alter->options = stmt->options;
+			alter->action = 1;
+
+			table_close(pg_authid_rel, NoLock);
+
+			return AlterRole(pstate, alter);
+		}
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_OBJECT),
+					 errmsg("role \"%s\" already exists",
+							stmt->role)));
+	}
 
 	/* Convert validuntil to internal form */
 	if (validUntil)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index ad1ea2ff2f..e1c4f90276 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4516,6 +4516,7 @@ _copyCreateRoleStmt(const CreateRoleStmt *from)
 	COPY_SCALAR_FIELD(stmt_type);
 	COPY_STRING_FIELD(role);
 	COPY_NODE_FIELD(options);
+	COPY_SCALAR_FIELD(replace);
 
 	return newnode;
 }
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index f537d3eb96..e6224156af 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2129,6 +2129,7 @@ _equalCreateRoleStmt(const CreateRoleStmt *a, const CreateRoleStmt *b)
 	COMPARE_SCALAR_FIELD(stmt_type);
 	COMPARE_STRING_FIELD(role);
 	COMPARE_NODE_FIELD(options);
+	COMPARE_SCALAR_FIELD(replace);
 
 	return true;
 }
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a6d0cefa6b..378fcacc19 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -1061,6 +1061,16 @@ CreateRoleStmt:
 					n->stmt_type = ROLESTMT_ROLE;
 					n->role = $3;
 					n->options = $5;
+					n->replace = false;
+					$$ = (Node *)n;
+				}
+			| CREATE OR REPLACE ROLE RoleId opt_with OptRoleList
+				{
+					CreateRoleStmt *n = makeNode(CreateRoleStmt);
+					n->stmt_type = ROLESTMT_ROLE;
+					n->role = $5;
+					n->options = $7;
+					n->replace = true;
 					$$ = (Node *)n;
 				}
 		;
@@ -1217,6 +1227,16 @@ CreateUserStmt:
 					n->stmt_type = ROLESTMT_USER;
 					n->role = $3;
 					n->options = $5;
+					n->replace = false;
+					$$ = (Node *)n;
+				}
+			| CREATE OR REPLACE USER RoleId opt_with OptRoleList
+				{
+					CreateRoleStmt *n = makeNode(CreateRoleStmt);
+					n->stmt_type = ROLESTMT_USER;
+					n->role = $5;
+					n->options = $7;
+					n->replace = true;
 					$$ = (Node *)n;
 				}
 		;
@@ -1356,6 +1376,16 @@ CreateGroupStmt:
 					n->stmt_type = ROLESTMT_GROUP;
 					n->role = $3;
 					n->options = $5;
+					n->replace = false;
+					$$ = (Node *)n;
+				}
+			| CREATE OR REPLACE GROUP_P RoleId opt_with OptRoleList
+				{
+					CreateRoleStmt *n = makeNode(CreateRoleStmt);
+					n->stmt_type = ROLESTMT_GROUP;
+					n->role = $5;
+					n->options = $7;
+					n->replace = true;
 					$$ = (Node *)n;
 				}
 		;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 067138e6b5..b953d092ae 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2622,6 +2622,7 @@ typedef struct CreateRoleStmt
 	RoleStmtType stmt_type;		/* ROLE/USER/GROUP */
 	char	   *role;			/* role name */
 	List	   *options;		/* List of DefElem nodes */
+	bool       replace;			/* Merge new definition into existing role */
 } CreateRoleStmt;
 
 typedef struct AlterRoleStmt
diff --git a/src/test/regress/expected/roleattributes.out b/src/test/regress/expected/roleattributes.out
index 5e6969b173..a81c999062 100644
--- a/src/test/regress/expected/roleattributes.out
+++ b/src/test/regress/expected/roleattributes.out
@@ -247,3 +247,51 @@ DROP ROLE regress_test_def_replication;
 DROP ROLE regress_test_replication;
 DROP ROLE regress_test_def_bypassrls;
 DROP ROLE regress_test_bypassrls;
+-- CREATE OR REPLACE ROLE
+CREATE ROLE regress_test_exists_role;
+CREATE ROLE regress_test_exists_role;
+ERROR:  role "regress_test_exists_role" already exists
+CREATE OR REPLACE ROLE regress_test_exists_role;
+DROP ROLE regress_test_exists_role;
+CREATE ROLE regress_test_exists_role WITH NOINHERIT;
+CREATE OR REPLACE ROLE regress_test_exists_role WITH INHERIT;
+SELECT rolname, rolinherit FROM pg_authid WHERE rolname = 'regress_test_exists_role';
+         rolname          | rolinherit 
+--------------------------+------------
+ regress_test_exists_role | t
+(1 row)
+
+ALTER ROLE regress_test_exists_role WITH INHERIT;
+SELECT rolname, rolinherit FROM pg_authid WHERE rolname = 'regress_test_exists_role';
+         rolname          | rolinherit 
+--------------------------+------------
+ regress_test_exists_role | t
+(1 row)
+
+DROP ROLE regress_test_exists_role;
+CREATE USER regress_test_exists_user;
+CREATE USER regress_test_exists_user;
+ERROR:  role "regress_test_exists_user" already exists
+CREATE OR REPLACE USER regress_test_exists_user;
+DROP USER regress_test_exists_user;
+CREATE USER regress_test_exists_user WITH NOINHERIT;
+CREATE OR REPLACE USER regress_test_exists_user WITH INHERIT;
+SELECT rolname, rolinherit FROM pg_authid WHERE rolname = 'regress_test_exists_user';
+         rolname          | rolinherit 
+--------------------------+------------
+ regress_test_exists_user | t
+(1 row)
+
+ALTER USER regress_test_exists_user WITH INHERIT;
+SELECT rolname, rolinherit FROM pg_authid WHERE rolname = 'regress_test_exists_user';
+         rolname          | rolinherit 
+--------------------------+------------
+ regress_test_exists_user | t
+(1 row)
+
+DROP USER regress_test_exists_user;
+CREATE GROUP regress_test_exists_group;
+CREATE GROUP regress_test_exists_group;
+ERROR:  role "regress_test_exists_group" already exists
+CREATE OR REPLACE GROUP regress_test_exists_group;
+DROP GROUP regress_test_exists_group;
diff --git a/src/test/regress/sql/roleattributes.sql b/src/test/regress/sql/roleattributes.sql
index c961b2d730..185931f413 100644
--- a/src/test/regress/sql/roleattributes.sql
+++ b/src/test/regress/sql/roleattributes.sql
@@ -96,3 +96,31 @@ DROP ROLE regress_test_def_replication;
 DROP ROLE regress_test_replication;
 DROP ROLE regress_test_def_bypassrls;
 DROP ROLE regress_test_bypassrls;
+
+-- CREATE OR REPLACE ROLE
+CREATE ROLE regress_test_exists_role;
+CREATE ROLE regress_test_exists_role;
+CREATE OR REPLACE ROLE regress_test_exists_role;
+DROP ROLE regress_test_exists_role;
+CREATE ROLE regress_test_exists_role WITH NOINHERIT;
+CREATE OR REPLACE ROLE regress_test_exists_role WITH INHERIT;
+SELECT rolname, rolinherit FROM pg_authid WHERE rolname = 'regress_test_exists_role';
+ALTER ROLE regress_test_exists_role WITH INHERIT;
+SELECT rolname, rolinherit FROM pg_authid WHERE rolname = 'regress_test_exists_role';
+DROP ROLE regress_test_exists_role;
+
+CREATE USER regress_test_exists_user;
+CREATE USER regress_test_exists_user;
+CREATE OR REPLACE USER regress_test_exists_user;
+DROP USER regress_test_exists_user;
+CREATE USER regress_test_exists_user WITH NOINHERIT;
+CREATE OR REPLACE USER regress_test_exists_user WITH INHERIT;
+SELECT rolname, rolinherit FROM pg_authid WHERE rolname = 'regress_test_exists_user';
+ALTER USER regress_test_exists_user WITH INHERIT;
+SELECT rolname, rolinherit FROM pg_authid WHERE rolname = 'regress_test_exists_user';
+DROP USER regress_test_exists_user;
+
+CREATE GROUP regress_test_exists_group;
+CREATE GROUP regress_test_exists_group;
+CREATE OR REPLACE GROUP regress_test_exists_group;
+DROP GROUP regress_test_exists_group;
-- 
2.30.1 (Apple Git-130)

