Créer un ROLE PostgreSQL (utilisateur) s'il n'existe pas

123

Comment écrire un script SQL pour créer un ROLE dans PostgreSQL 9.1, mais sans générer d'erreur s'il existe déjà?

Le script actuel a simplement:

CREATE ROLE my_user LOGIN PASSWORD 'my_password';

Cela échoue si l'utilisateur existe déjà. J'aimerais quelque chose comme:

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

... mais cela ne fonctionne pas - IFne semble pas être pris en charge en SQL brut.

J'ai un fichier batch qui crée une base de données PostgreSQL 9.1, un rôle et quelques autres choses. Il appelle psql.exe, en passant le nom d'un script SQL à exécuter. Jusqu'à présent, tous ces scripts sont du SQL pur et j'aimerais éviter PL / pgSQL et autres, si possible.

EMP
la source

Réponses:

157

Simplifiez de la même manière que ce que vous aviez en tête:

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$;

(S'appuyant sur la réponse de @a_horse_with_no_name et amélioré avec le commentaire de @ Gregory .)

Contrairement à, par exemple, CREATE TABLEil n'y a pas de IF NOT EXISTSclause pour CREATE ROLE(jusqu'à au moins pg 12). Et vous ne pouvez pas exécuter des instructions DDL dynamiques en SQL brut.

Votre demande "d'éviter PL / pgSQL" est impossible sauf en utilisant un autre PL. L' DOinstruction utilise plpgsql comme langage procédural par défaut. La syntaxe permet d'omettre la déclaration explicite:

DO [ LANGUAGE lang_name ] code
... Le nom du langage procédural dans lequel le code est écrit. S'il est omis, la valeur par défaut est .
lang_name
plpgsql

Erwin Brandstetter
la source
1
@Alberto: pg_user et pg_roles sont tous les deux corrects. Toujours le cas dans la version actuelle 9.3 et cela ne changera pas de sitôt.
Erwin Brandstetter
2
@Ken: Si $a une signification particulière dans votre client, vous devez l'échapper selon les règles de syntaxe de votre client. Essayez de vous échapper $avec \$dans le shell Linux. Ou commencez une nouvelle question - les commentaires ne sont pas l'endroit. Vous pouvez toujours créer un lien vers celui-ci pour le contexte.
Erwin Brandstetter
1
J'utilise 9.6, et si un utilisateur a été créé avec NOLOGIN, il n'apparaît pas dans la table pg_user, mais apparaît dans la table pg_roles. Est-ce que pg_roles serait une meilleure solution ici?
Jess
2
@ErwinBrandstetter Cela ne fonctionne pas pour les rôles qui ont NOLOGIN. Ils apparaissent dans pg_roles mais pas dans pg_user.
Gregory Arenius
2
Cette solution souffre d'une condition de course. Une variante plus sûre est documentée dans cette réponse .
blubb
61

La réponse acceptée souffre d'une condition de concurrence si deux de ces scripts sont exécutés simultanément sur le même cluster Postgres (serveur de base de données), comme cela est courant dans les environnements d'intégration continue .

Il est généralement plus sûr d'essayer de créer le rôle et de gérer les problèmes avec élégance lors de sa création:

DO $$
BEGIN
  CREATE ROLE my_role WITH NOLOGIN;
  EXCEPTION WHEN DUPLICATE_OBJECT THEN
  RAISE NOTICE 'not creating role my_role -- it already exists';
END
$$;
blubb
la source
2
J'aime cette façon parce qu'elle notifie l'existence.
Matias Barone
2
DUPLICATE_OBJECTest la condition précise dans ce cas, si vous ne voulez pas capturer à peu près toutes les conditions avec OTHERS.
Danek Duvall le
43

Ou si le rôle n'est le propriétaire d'aucun objet de base de données, on peut utiliser:

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

Mais seulement si la suppression de cet utilisateur ne fera aucun mal.

Borys
la source
10

Alternative à Bash (pour les scripts 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';"

(n'est pas la réponse à la question! c'est seulement pour ceux qui peuvent être utiles)

Eduardo Cuomo
la source
3
Il devrait lire à la FROM pg_roles WHERE rolnameplace deFROM pg_user WHERE usename
Barth
8

Voici une solution générique utilisant 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;

Usage:

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
la source
8

Quelques réponses suggèrent d'utiliser le modèle: vérifiez si le rôle n'existe pas et si ce n'est pas le cas, lancez la CREATE ROLEcommande. Cela a un inconvénient: la condition de course. Si quelqu'un d'autre crée un nouveau rôle entre la vérification et l'émission de la CREATE ROLEcommande, il CREATE ROLEéchoue évidemment avec une erreur fatale.

Pour résoudre le problème ci-dessus, d'autres réponses ont déjà mentionné l'utilisation de PL/pgSQL, l'émission CREATE ROLEinconditionnelle, puis la capture des exceptions de cet appel. Il n'y a qu'un seul problème avec ces solutions. Ils abandonnent silencieusement toutes les erreurs, y compris celles qui ne sont pas générées par le fait que ce rôle existe déjà. CREATE ROLEpeut également générer d'autres erreurs et la simulation IF NOT EXISTSne devrait faire taire l'erreur que lorsque le rôle existe déjà.

CREATE ROLElancer une duplicate_objecterreur lorsque le rôle existe déjà. Et le gestionnaire d'exceptions ne doit intercepter que cette seule erreur. Comme d'autres réponses l'ont mentionné, il est judicieux de convertir l'erreur fatale en simple avis. D'autres IF NOT EXISTScommandes PostgreSQL s'ajoutent , skippingà leur message, donc par souci de cohérence, je les ajoute ici aussi.

Voici le code SQL complet pour la simulation CREATE ROLE IF NOT EXISTSavec l'exception correcte et la propagation sqlstate:

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

Sortie de test (appelée deux fois via DO puis directement):

$ 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
la source
2
Je vous remercie. Aucune condition de course, capture d'exception serrée, encapsulation du propre message de Postgres au lieu de réécrire le vôtre.
Stefano Taschini
1
En effet! C'est actuellement la seule réponse correcte ici, qui ne souffre pas des conditions de course et utilise la gestion sélective des erreurs nécessaire. Il est vraiment dommage que cette réponse soit apparue après que la première réponse (pas tout à fait correcte) ait recueilli plus de 100 points.
vog
1
Vous êtes les bienvenus! Ma solution propage également SQLSTATE, donc si vous appelez une instruction à partir d'un autre script PL / SQL ou d'un autre langage avec un connecteur SQL, vous recevrez SQLSTATE correct.
Pali
6

Comme vous êtes sur 9.x, vous pouvez encapsuler cela dans une instruction 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 cheval sans nom
la source
Sélectionnez devrait être `SELECT count (*) into num_users FROM pg_roles WHERE rolname = 'data_rw';` Sinon, cela ne fonctionnera pas
Miro
6

Mon équipe rencontrait une situation avec plusieurs bases de données sur un serveur, selon la base de données à laquelle vous vous êtes connecté, le ROLE en question n'a pas été renvoyé par SELECT * FROM pg_catalog.pg_user, comme proposé par @ erwin-brandstetter et @a_horse_with_no_name. Le bloc conditionnel s'est exécuté et nous avons frappé role "my_user" already exists.

Malheureusement, nous ne sommes pas sûrs des conditions exactes, mais cette solution résout le problème:

        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$

Il pourrait probablement être rendu plus précis pour exclure d'autres exceptions.

Chris Betti
la source
3
La table pg_user semble inclure uniquement les rôles qui ont LOGIN. Si un rôle a NOLOGIN, il n'apparaît pas dans pg_user, du moins dans PostgreSQL 10.
Gregory Arenius
2

Vous pouvez le faire dans votre fichier de commandes en analysant la sortie de:

SELECT * FROM pg_user WHERE usename = 'my_user'

puis réexécutez psql.exesi le rôle n'existe pas.

Sheva
la source
2
La colonne "nom d'utilisateur" n'existe pas. Ce devrait être "nom d'utilisateur".
Mouhammed Soueidane
3
"nom d'utilisateur" est celui qui n'existe pas. :)
Garen
1
Veuillez vous référer au doc de vue pg_user . Il n'y a pas de colonne «nom d'utilisateur» dans les versions 7.4-9.6, «nom d'utilisateur» est la bonne.
Sheva
1

La même solution que pour Simulate CREATE DATABASE IF NOT EXISTS pour PostgreSQL? devrait fonctionner - envoyez un CREATE USER …à \gexec.

Solution de contournement à partir de psql

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

Solution de contournement depuis le shell

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

Voir la réponse acceptée ici pour plus de détails.

Alexandre Skwar
la source
Votre solution a toujours une condition de concurrence que j'ai décrite dans ma réponse stackoverflow.com/a/55954480/7878845 Si vous exécutez votre script shell en parallèle plusieurs fois, vous obtenez ERREUR: le rôle "my_user" existe déjà
Pali