Utwórz rolę PostgreSQL (użytkownika), jeśli nie istnieje

122

Jak napisać skrypt SQL w celu utworzenia roli w PostgreSQL 9.1, ale bez zgłaszania błędu, jeśli już istnieje?

Obecny skrypt ma po prostu:

CREATE ROLE my_user LOGIN PASSWORD 'my_password';

Nie powiedzie się, jeśli użytkownik już istnieje. Chciałbym coś takiego:

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

... ale to nie działa - IFnie wydaje się być obsługiwane w zwykłym języku SQL.

Mam plik wsadowy, który tworzy bazę danych PostgreSQL 9.1, rolę i kilka innych rzeczy. Wywołuje psql.exe, przekazując nazwę skryptu SQL do uruchomienia. Jak dotąd wszystkie te skrypty to zwykły SQL i chciałbym unikać PL / pgSQL i tym podobnych, jeśli to możliwe.

EMP
źródło

Odpowiedzi:

156

Uprość w podobny sposób, jak miałeś na myśli:

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

(Opierając się na odpowiedzi @a_horse_with_no_name i ulepszonym komentarzem @ Gregory .)

W przeciwieństwie na przykład do CREATE TABLEbraku IF NOT EXISTSklauzuli for CREATE ROLE(do co najmniej str. 12). Nie można też wykonywać dynamicznych instrukcji DDL w zwykłym języku SQL.

Twoja prośba o „uniknięcie PL / pgSQL” jest niemożliwa z wyjątkiem użycia innego PL. Do DOrachunku zastosowania plpgsql jako domyślny język proceduralny. Składnia pozwala na pominięcie jawnej deklaracji:

DO [ LANGUAGE lang_name ] code
... Nazwa języka proceduralnego, w którym zapisywany jest kod. Jeśli zostanie pominięty, wartością domyślną jest .
lang_name
plpgsql

Erwin Brandstetter
źródło
1
@Alberto: pg_user i pg_roles są poprawne. Wciąż tak jest w obecnej wersji 9.3 i nie zmieni się to w najbliższym czasie.
Erwin Brandstetter
2
@Ken: Jeśli $ma specjalne znaczenie w twoim kliencie, musisz go uciec zgodnie z regułami składni twojego klienta. Spróbuj uciec $z \$w powłoce Linuksa. Lub zacznij nowe pytanie - komentarze nie są miejscem. Zawsze możesz utworzyć link do tego, aby uzyskać kontekst.
Erwin Brandstetter
1
Używam 9.6 i jeśli użytkownik został utworzony za pomocą NOLOGIN, nie pojawiają się w tabeli pg_user, ale pojawiają się w tabeli pg_roles. Czy pg_roles byłoby tutaj lepszym rozwiązaniem?
Jess
2
@ErwinBrandstetter To nie działa w przypadku ról, które mają NOLOGIN. Pojawiają się w pg_roles, ale nie w pg_user.
Gregory Arenius
2
To rozwiązanie cierpi z powodu wyścigu. W tej odpowiedzi udokumentowano bezpieczniejszy wariant .
blubb
60

Zaakceptowana odpowiedź cierpi z powodu sytuacji wyścigu, jeśli dwa takie skrypty są wykonywane jednocześnie w tym samym klastrze Postgres (serwerze DB), co jest powszechne w środowiskach ciągłej integracji .

Generalnie bezpieczniej jest spróbować utworzyć rolę i z wdziękiem rozwiązywać problemy podczas jej tworzenia:

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
źródło
2
Podoba mi się ten sposób, ponieważ zgłasza istnienie.
Matias Barone
2
DUPLICATE_OBJECTto dokładny warunek w tym przypadku, jeśli nie chcesz złapać prawie wszystkich warunków OTHERS.
Danek Duvall
43

Lub jeśli rola nie jest właścicielem żadnych obiektów bazy danych, których można użyć:

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

Ale tylko wtedy, gdy upuszczenie tego użytkownika nie spowoduje żadnej szkody.

Borys
źródło
10

Alternatywa Bash (dla skryptów 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';"

(nie jest odpowiedzią na pytanie! jest tylko dla tych, którzy mogą się przydać)

Eduardo Cuomo
źródło
3
Powinien być czytany FROM pg_roles WHERE rolnamezamiastFROM pg_user WHERE usename
Barth
8

Oto ogólne rozwiązanie wykorzystujące 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;

Stosowanie:

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
źródło
8

Niektóre odpowiedzi sugerowały użycie wzorca: sprawdź, czy rola nie istnieje, a jeśli nie, wydaj CREATE ROLEpolecenie. Ma to jedną wadę: stan wyścigu. Jeśli ktoś inny utworzy nową rolę między sprawdzeniem a wydaniem CREATE ROLEpoleceniaCREATE ROLE oczywiście nie powiedzie się z błędem krytycznym.

Aby rozwiązać powyższy problem, więcej innych odpowiedzi wspominało już o użyciu PL/pgSQL, wydawaniu CREATE ROLEbezwarunkowym, a następnie wyłapywaniu wyjątków od tego połączenia. Z tymi rozwiązaniami jest tylko jeden problem. Po cichu odrzucają wszelkie błędy, w tym te, które nie są generowane przez fakt, że rola już istnieje. CREATE ROLEmoże generować również inne błędy, a symulacja IF NOT EXISTSpowinna wyciszać błędy tylko wtedy, gdy rola już istnieje.

CREATE ROLEzgłosić duplicate_objectbłąd, gdy rola już istnieje. Program obsługi wyjątków powinien wychwycić tylko ten jeden błąd. Jak wspomniały inne odpowiedzi, dobrym pomysłem jest przekształcenie błędu krytycznego w proste powiadomienie. Inne IF NOT EXISTSpolecenia PostgreSQL są dodawane , skippingdo ich wiadomości, więc dla spójności dodam je tutaj.

Oto pełny kod SQL do symulacji CREATE ROLE IF NOT EXISTSz poprawnym wyjątkiem i propagacją stanu sql :

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

Wyjście testowe (wywoływane dwukrotnie przez DO, a następnie bezpośrednio):

$ 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
źródło
2
Dziękuję Ci. Brak warunków wyścigu, ścisły chwyt wyjątków, zawijanie własnej wiadomości Postgresa zamiast przepisywania własnej.
Stefano Taschini
1
W rzeczy samej! Jest to obecnie jedyna poprawna odpowiedź, która nie cierpi z powodu warunków wyścigu i wykorzystuje niezbędną selektywną obsługę błędów. Szkoda, że ​​ta odpowiedź pojawiła się po (nie do końca poprawnej) odpowiedzi górnej, która zebrała ponad 100 punktów.
vog,
1
Zapraszamy! Moje rozwiązanie również propaguje SQLSTATE, więc jeśli wywołujesz instrukcję z innego skryptu PL / SQL lub innego języka z łącznikiem SQL, otrzymasz poprawny SQLSTATE.
Pali
6

Ponieważ korzystasz z 9.x, możesz zawrzeć to w oświadczeniu 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$
;
koń bez imienia
źródło
Wybierz powinno być `SELECT count (*) into num_users FROM pg_roles WHERE rolname = 'data_rw';` W przeciwnym razie to nie zadziała
Miro
6

Mój zespół miał problem z wieloma bazami danych na jednym serwerze, w zależności od tego, z którą bazą danych się SELECT * FROM pg_catalog.pg_userłączyłeś , dana ROLA nie została zwrócona , zgodnie z propozycjami @ erwin-brandstetter i @a_horse_with_no_name. Blok warunkowy został wykonany i trafiliśmy role "my_user" already exists.

Niestety nie jesteśmy pewni dokładnych warunków, ale to rozwiązanie pozwala obejść problem:

        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$

Prawdopodobnie można by było doprecyzować, aby wykluczyć inne wyjątki.

Chris Betti
źródło
3
Tabela pg_user wydaje się zawierać tylko role, które mają LOGIN. Jeśli rola ma NOLOGIN, nie pojawia się w pg_user, przynajmniej w PostgreSQL 10.
Gregory Arenius
2

Możesz to zrobić w swoim pliku wsadowym, analizując dane wyjściowe:

SELECT * FROM pg_user WHERE usename = 'my_user'

a następnie uruchamiany psql.exeponownie, jeśli rola nie istnieje.

Sheva
źródło
2
Kolumna „nazwa użytkownika” nie istnieje. Powinien to być „nazwa użytkownika”.
Mouhammed Soueidane
3
„nazwa użytkownika” to ta, która nie istnieje. :)
Garen
1
Proszę odnieść się do pg_user widoku dok. W wersjach 7.4-9.6 nie ma kolumny „nazwa użytkownika”, poprawna jest „nazwa użytkownika”.
Sheva
1

Takie samo rozwiązanie jak w przypadku Symuluj STWÓRZ BAZY DANYCH, JEŚLI NIE ISTNIEJE dla PostgreSQL? powinno działać - wyślij CREATE USER …do \gexec.

Obejście z poziomu psql

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

Obejście z powłoki

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

Zobacz zaakceptowaną odpowiedź, aby uzyskać więcej informacji.

Alexander Skwar
źródło
Twoje rozwiązanie nadal ma stan wyścigu, który opisałem w mojej odpowiedzi stackoverflow.com/a/55954480/7878845 Jeśli uruchomisz skrypt powłoki równolegle więcej razy, otrzymasz BŁĄD: rola "my_user" już istnieje
Pali