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à. Je voudrais 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 - IF
ne semble pas être pris en charge en langage SQL simple.
J'ai un fichier de commandes 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 en langage SQL simple et j'aimerais éviter PL/pgSQL et autres, si possible.
Simplifiez-vous de la même manière que ce que vous aviez en tête:
DO
$do$
BEGIN
IF NOT EXISTS (
SELECT -- SELECT list can stay empty for this
FROM pg_catalog.pg_roles
WHERE rolname = 'my_user') THEN
CREATE ROLE my_user LOGIN PASSWORD 'my_password';
END IF;
END
$do$;
(Construit sur la réponse de @ a_horse_with_no_name et amélioré après le commentaire de @ Gregory .)
Contrairement à, par exemple, avec CREATE TABLE
il n'y a pas de IF NOT EXISTS
clause pour CREATE ROLE
(encore). Et vous ne pouvez pas exécuter des instructions DDL dynamiques en langage SQL simple.
Votre demande "d'éviter PL/pgSQL" est impossible sauf en utilisant un autre PL. La déclaration DO
utilise plpgsql comme langage procédural par défaut. La syntaxe permet d'omettre la déclaration explicite:
DO [ LANGUAGE
lang_name
] code
...lang_name
Nom du langage procédural dans lequel le code est écrit. S'il est omis, la valeur par défaut estplpgsql
.
Ou si le rôle n’est propriétaire d’objets 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 laisser tomber cet utilisateur ne fera aucun mal.
La réponse acceptée souffre d'une situation critique si deux scripts de ce type sont exécutés simultanément sur le même cluster Postgres (serveur de base de données), comme il est courant dans les environnements à intégration continue.
Il est généralement plus sûr d'essayer de créer le rôle et de résoudre les problèmes avec élégance lors de sa création:
DO $$
BEGIN
CREATE ROLE my_role WITH NOLOGIN;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'not creating role my_role -- it already exists';
END
$$;
Bash alternative (pour script 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-ce pas la réponse à la question! c'est seulement pour ceux qui peuvent être utiles)
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)
Comme vous êtes sur 9.x, vous pouvez en faire une déclaration:
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$
;
Mon équipe était confrontée à 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
, tel que proposé par @ erwin-brandstetter et @a_horse_with_no_name. Le bloc conditionnel exécuté, et nous avons frappé role "my_user" already exists
.
Malheureusement, nous ne sommes pas sûrs des conditions exactes, mais cette solution contourne 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 spécifique pour exclure d'autres exceptions.
Vous pouvez le faire dans votre fichier de commandes en analysant la sortie de:
SELECT * FROM pg_user WHERE usename = 'my_user'
puis en cours d'exécution psql.exe
encore une fois si le rôle n'existe pas.
Certaines réponses suggèrent d’utiliser pattern: vérifie si le rôle n’existe pas et si non, lancez CREATE ROLE
commande. Cela a un inconvénient: la condition de concurrence. Si quelqu'un d'autre crée un nouveau rôle entre check et émission CREATE ROLE
commande puis CREATE ROLE
échoue évidemment avec une erreur fatale.
Pour résoudre le problème ci-dessus, plusieurs autres réponses ont déjà mentionné l'utilisation de PL/pgSQL
, émettant CREATE ROLE
_ inconditionnellement, puis intercepter des exceptions de cet appel. Il n'y a qu'un problème avec ces solutions. Ils suppriment 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 ROLE
peut également générer d’autres erreurs et simulations IF NOT EXISTS
devrait ne taire que les erreurs lorsque le rôle existe déjà.
CREATE ROLE
jeter duplicate_object
_ erreur lorsque le rôle existe déjà. Et le gestionnaire d'exceptions ne devrait intercepter que cette seule erreur. Comme d'autres réponses l'ont mentionné, il est judicieux de convertir une erreur fatale en un simple avis. Autre PostgreSQL IF NOT EXISTS
commandes ajoute , skipping
_ dans leur message, donc pour plus de cohérence, je l’ajoute ici aussi.
Voici le code SQL complet pour la simulation de CREATE ROLE IF NOT EXISTS
avec exception correcte et propagation de SQLSTATE:
DO $$
BEGIN
CREATE ROLE test;
EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
END
$$;
Test de la sortie (appelé 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
La même solution que pour Simuler CREATE DATABASE SI PAS EXISTS pour PostgreSQL? devrait fonctionner - envoyer un CREATE USER …
à \gexec
.
SELECT 'CREATE USER my_user'
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'my_user')\gexec
echo "SELECT 'CREATE USER my_user' WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'my_user')\gexec" | psql
Voir réponse acceptée ici pour plus de détails.