Cree PostgreSQL ROLE (usuario) si no existe

122

¿Cómo escribo un script SQL para crear un ROL en PostgreSQL 9.1, pero sin generar un error si ya existe?

El script actual simplemente tiene:

CREATE ROLE my_user LOGIN PASSWORD 'my_password';

Esto falla si el usuario ya existe. Me gustaría algo como:

IF NOT EXISTS (SELECT * FROM pg_user WHERE username = 'my_user')
BEGIN
    CREATE ROLE my_user LOGIN PASSWORD 'my_password';
END;

... pero eso no funciona, IFno parece ser compatible con SQL simple.

Tengo un archivo por lotes que crea una base de datos PostgreSQL 9.1, un rol y algunas otras cosas. Llama a psql.exe, pasando el nombre de un script SQL para ejecutar. Hasta ahora, todos estos scripts son SQL simple y me gustaría evitar PL / pgSQL y demás, si es posible.

EMP
fuente

Respuestas:

156

Simplifique de manera similar a lo que tenía en mente:

DO
$do$
BEGIN
   IF NOT EXISTS (
      SELECT FROM pg_catalog.pg_roles  -- SELECT list can be empty for this
      WHERE  rolname = 'my_user') THEN

      CREATE ROLE my_user LOGIN PASSWORD 'my_password';
   END IF;
END
$do$;

(Basado en la respuesta de @a_horse_with_no_name y mejorado con el comentario de @ Gregory ).

A diferencia de, por ejemplo, con CREATE TABLEno hay una IF NOT EXISTScláusula para CREATE ROLE(hasta al menos la página 12). Y no puede ejecutar declaraciones DDL dinámicas en SQL simple.

Su solicitud de "evitar PL / pgSQL" es imposible excepto si usa otro PL. La DOdeclaración usa plpgsql como lenguaje de procedimiento predeterminado. La sintaxis permite omitir la declaración explícita:

DO [ LANGUAGE lang_name ] code
... El nombre del lenguaje de procedimiento en el que está escrito el código. Si se omite, el predeterminado es .
lang_name
plpgsql

Erwin Brandstetter
fuente
1
@Alberto: pg_user y pg_roles son correctos. Sigue siendo el caso en la versión actual 9.3 y no va a cambiar pronto.
Erwin Brandstetter
2
@Ken: Si $tiene un significado especial en su cliente, debe escapar de acuerdo con las reglas de sintaxis de su cliente. Intente escapar $con \$el shell de Linux. O comience una nueva pregunta: los comentarios no son el lugar. Siempre puede vincular a este para el contexto.
Erwin Brandstetter
1
Estoy usando 9.6, y si se creó un usuario con NOLOGIN, no aparece en la tabla pg_user, pero sí en la tabla pg_roles. ¿Sería pg_roles una mejor solución aquí?
Jess
2
@ErwinBrandstetter Esto no funciona para roles que tienen NOLOGIN. Aparecen en pg_roles pero no en pg_user.
Gregory Arenius
2
Esta solución adolece de una condición de carrera. En esta respuesta se documenta una variante más segura .
blubb
60

La respuesta aceptada sufre una condición de carrera si dos de estos scripts se ejecutan simultáneamente en el mismo clúster de Postgres (servidor de base de datos), como es común en los entornos de integración continua .

Por lo general, es más seguro intentar crear el rol y tratar con elegancia los problemas al crearlo:

DO $$
BEGIN
  CREATE ROLE my_role WITH NOLOGIN;
  EXCEPTION WHEN DUPLICATE_OBJECT THEN
  RAISE NOTICE 'not creating role my_role -- it already exists';
END
$$;
lloriquear
fuente
2
Me gusta así porque notifica que existen.
Matias Barone
2
DUPLICATE_OBJECTes la condición precisa en este caso, si no desea detectar casi todas las condiciones con OTHERS.
Danek Duvall
43

O si el rol no es el propietario de ningún objeto db, se puede usar:

DROP ROLE IF EXISTS my_user;
CREATE ROLE my_user LOGIN PASSWORD 'my_password';

Pero solo si dejar caer a este usuario no hará ningún daño.

Borys
fuente
10

Alternativa de Bash (para scripts de Bash ):

psql -h localhost -U postgres -tc \
"SELECT 1 FROM pg_user WHERE usename = 'my_user'" \
| grep -q 1 \
|| psql -h localhost -U postgres \
-c "CREATE ROLE my_user LOGIN PASSWORD 'my_password';"

(¡No es la respuesta a la pregunta! Es solo para aquellos que pueden ser útiles)

Eduardo Cuomo
fuente
3
Debería leerse en FROM pg_roles WHERE rolnamelugar deFROM pg_user WHERE usename
Barth
8

Aquí hay una solución genérica que usa plpgsql:

CREATE OR REPLACE FUNCTION create_role_if_not_exists(rolename NAME) RETURNS TEXT AS
$$
BEGIN
    IF NOT EXISTS (SELECT * FROM pg_roles WHERE rolname = rolename) THEN
        EXECUTE format('CREATE ROLE %I', rolename);
        RETURN 'CREATE ROLE';
    ELSE
        RETURN format('ROLE ''%I'' ALREADY EXISTS', rolename);
    END IF;
END;
$$
LANGUAGE plpgsql;

Uso:

posgres=# SELECT create_role_if_not_exists('ri');
 create_role_if_not_exists 
---------------------------
 CREATE ROLE
(1 row)
posgres=# SELECT create_role_if_not_exists('ri');
 create_role_if_not_exists 
---------------------------
 ROLE 'ri' ALREADY EXISTS
(1 row)
Wolkenarchitekt
fuente
8

Algunas respuestas sugirieron usar patrón: verifique si el rol no existe y si no, emita el CREATE ROLEcomando. Esto tiene una desventaja: condición de carrera. Si alguien más crea un nuevo rol entre la verificación y la emisión del CREATE ROLEcomando,CREATE ROLE obviamente falla con un error fatal.

Para resolver el problema anterior, más otras respuestas ya mencionaron el uso de PL/pgSQL, emitiendo CREATE ROLEincondicionalmente y luego capturando excepciones de esa llamada. Solo hay un problema con estas soluciones. Eliminan silenciosamente cualquier error, incluidos los que no se generan por el hecho de que el rol ya existe. CREATE ROLEpuede arrojar también otros errores y la simulación IF NOT EXISTSdebe silenciar solo el error cuando el rol ya existe.

CREATE ROLEarrojar duplicate_objecterror cuando el rol ya existe. Y el controlador de excepciones debería detectar solo este error. Como se mencionó en otras respuestas, es una buena idea convertir un error fatal en un simple aviso. Se IF NOT EXISTSagregan otros comandos de PostgreSQL, skipping a su mensaje, por lo que para mantener la coherencia, también lo agrego aquí.

Aquí está el código SQL completo para la simulación CREATE ROLE IF NOT EXISTScon la excepción correcta y la propagación de sqlstate:

DO $$
BEGIN
CREATE ROLE test;
EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
END
$$;

Salida de prueba (llamada dos veces a través de DO y luego directamente):

$ sudo -u postgres psql
psql (9.6.12)
Type "help" for help.

postgres=# \set ON_ERROR_STOP on
postgres=# \set VERBOSITY verbose
postgres=# 
postgres=# DO $$
postgres$# BEGIN
postgres$# CREATE ROLE test;
postgres$# EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
postgres$# END
postgres$# $$;
DO
postgres=# 
postgres=# DO $$
postgres$# BEGIN
postgres$# CREATE ROLE test;
postgres$# EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
postgres$# END
postgres$# $$;
NOTICE:  42710: role "test" already exists, skipping
LOCATION:  exec_stmt_raise, pl_exec.c:3165
DO
postgres=# 
postgres=# CREATE ROLE test;
ERROR:  42710: role "test" already exists
LOCATION:  CreateRole, user.c:337
Pali
fuente
2
Gracias. Sin condiciones de carrera, captura de excepciones estrictas, envolviendo el propio mensaje de Postgres en lugar de reescribir el suyo.
Stefano Taschini
1
¡En efecto! Actualmente, esta es la única respuesta correcta aquí, que no sufre condiciones de carrera y utiliza el manejo de errores selectivo necesario. Es una lástima que esta respuesta apareciera después de que la respuesta principal (no del todo correcta) obtuviera más de 100 puntos.
vog
1
¡De nada! Mi solución también propaga SQLSTATE, por lo que si llama a una declaración desde otro script PL / SQL u otro lenguaje con conector SQL, recibirá SQLSTATE correcto.
Pali
6

Como está en 9.x, puede envolver eso en una declaración DO:

do 
$body$
declare 
  num_users integer;
begin
   SELECT count(*) 
     into num_users
   FROM pg_user
   WHERE usename = 'my_user';

   IF num_users = 0 THEN
      CREATE ROLE my_user LOGIN PASSWORD 'my_password';
   END IF;
end
$body$
;
un caballo sin nombre
fuente
Seleccione debe ser `SELECT count (*) en num_users FROM pg_roles WHERE rolname = 'data_rw';` De lo contrario, no funcionará
Miro
6

Mi equipo estaba enfrentando una situación con múltiples bases de datos en un servidor, según la base de datos a la que se conectó, el ROLE en cuestión no fue devuelto por SELECT * FROM pg_catalog.pg_user, como lo propusieron @ erwin-brandstetter y @a_horse_with_no_name. El bloque condicional se ejecutó y acertamos role "my_user" already exists.

Desafortunadamente, no estamos seguros de las condiciones exactas, pero esta solución soluciona el problema:

        DO  
        $body$
        BEGIN
            CREATE ROLE my_user LOGIN PASSWORD 'my_password';
        EXCEPTION WHEN others THEN
            RAISE NOTICE 'my_user role exists, not re-creating';
        END
        $body$

Probablemente podría hacerse más específico para descartar otras excepciones.

Chris Betti
fuente
3
La tabla pg_user parece incluir solo roles que tienen LOGIN. Si un rol tiene NOLOGIN, no aparece en pg_user, al menos en PostgreSQL 10.
Gregory Arenius
2

Puede hacerlo en su archivo por lotes analizando la salida de:

SELECT * FROM pg_user WHERE usename = 'my_user'

y luego ejecutarse psql.exeuna vez más si el rol no existe.

Sheva
fuente
2
La columna "nombre de usuario" no existe. Debe ser "usename".
Mouhammed Soueidane
3
"usename" es el que no existe. :)
Garen
1
Consulte el documento pg_user view. No hay una columna de "nombre de usuario" en las versiones 7.4-9.6, "usename" es la correcta.
Sheva
1

¿La misma solución que para Simulate CREATE DATABASE SI NO EXISTE para PostgreSQL? debería funcionar - enviar un CREATE USER …a \gexec.

Solución alternativa desde psql

SELECT 'CREATE USER my_user'
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'my_user')\gexec

Solución alternativa desde el caparazón

echo "SELECT 'CREATE USER my_user' WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'my_user')\gexec" | psql

Consulte la respuesta aceptada allí para obtener más detalles.

Alexander Skwar
fuente
Su solución aún tiene una condición de carrera que describí en mi respuesta stackoverflow.com/a/55954480/7878845 Si ejecuta su script de shell en paralelo más veces, obtiene ERROR: el rol "my_user" ya existe
Pali