Crie PostgreSQL ROLE (usuário) se não existir

122

Como escrevo um script SQL para criar um ROLE no PostgreSQL 9.1, mas sem gerar um erro se ele já existir?

O script atual simplesmente tem:

CREATE ROLE my_user LOGIN PASSWORD 'my_password';

Isso falhará se o usuário já existir. Eu gostaria de algo como:

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

... mas isso não funciona - IFnão parece ser suportado em SQL simples.

Eu tenho um arquivo em lote que cria um banco de dados PostgreSQL 9.1, função e algumas outras coisas. Ele chama psql.exe, passando o nome de um script SQL a ser executado. Até agora, todos esses scripts são SQL simples e, se possível, gostaria de evitar PL / pgSQL e tal.

EMP
fonte

Respostas:

156

Simplifique de forma semelhante ao que você tinha em 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$;

(Com base na resposta de @a_horse_with_no_name e melhorado com o comentário de @ Gregory .)

Ao contrário, por exemplo, de com CREATE TABLEnão há IF NOT EXISTScláusula para CREATE ROLE(até pelo menos página 12). E você não pode executar instruções DDL dinâmicas em SQL simples.

Sua solicitação para "evitar PL / pgSQL" é impossível, exceto usando outro PL. A DOinstrução usa plpgsql como linguagem procedural padrão. A sintaxe permite omitir a declaração explícita:

DO [ LANGUAGE lang_name ] code
... O nome da linguagem procedural em que o código está escrito. Se omitido, o padrão é .
lang_name
plpgsql

Erwin Brandstetter
fonte
1
@Alberto: pg_user e pg_roles estão corretos. Ainda é o caso na versão atual 9.3 e não vai mudar tão cedo.
Erwin Brandstetter
2
@Ken: se $tiver um significado especial em seu cliente, você precisa escapar dele de acordo com as regras de sintaxe de seu cliente. Tente escapar $com \$no shell do Linux. Ou comece uma nova pergunta - comentários não são o lugar. Você sempre pode criar um link para este para contexto.
Erwin Brandstetter
1
Estou usando o 9.6, e se um usuário foi criado com NOLOGIN, eles não aparecem na tabela pg_user, mas aparecem na tabela pg_roles. O pg_roles seria uma solução melhor aqui?
Jess
2
@ErwinBrandstetter Isso não funciona para funções que têm NOLOGIN. Eles aparecem no pg_roles, mas não no pg_user.
Gregory Arenius,
2
Esta solução sofre de uma condição de corrida. Uma variante mais segura está documentada nesta resposta .
blubb de
60

A resposta aceita sofre de uma condição de corrida se dois desses scripts são executados simultaneamente no mesmo cluster Postgres (servidor de banco de dados), como é comum em ambientes de integração contínua .

Geralmente é mais seguro tentar criar a função e lidar com problemas ao criá-la:

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
fonte
2
Gosto desta forma porque avisa que existem.
Matias Barone
2
DUPLICATE_OBJECTé a condição precisa neste caso, se você não quiser capturar quase todas as condições com OTHERS.
Danek Duvall
43

Ou se a função não for o proprietário de nenhum objeto db, pode-se usar:

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

Mas somente se deixar cair este usuário não causará nenhum dano.

Borys
fonte
10

Alternativa Bash (para 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ão é a resposta para a pergunta! é apenas para aqueles que podem ser úteis)

Eduardo cuomo
fonte
3
Deve ser lido em FROM pg_roles WHERE rolnamevez deFROM pg_user WHERE usename
Barth
8

Aqui está uma solução genérica usando 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
fonte
8

Algumas respostas sugeriram usar o padrão: verifique se a função não existe e se não, emita o CREATE ROLEcomando. Isso tem uma desvantagem: condição de corrida. Se outra pessoa criar uma nova função entre verificar e emitir o CREATE ROLEcomando, CREATE ROLEobviamente falhará com um erro fatal.

Para resolver o problema acima, mais outras respostas já mencionaram o uso de PL/pgSQL, emitindo CREATE ROLEincondicionalmente e, em seguida, pegando exceções dessa chamada. Existe apenas um problema com essas soluções. Eles silenciosamente eliminam quaisquer erros, incluindo aqueles que não são gerados pelo fato de que a função já existe. CREATE ROLEtambém pode lançar outros erros e a simulação IF NOT EXISTSdeve silenciar apenas o erro quando a função já existe.

CREATE ROLElançar duplicate_objecterro quando a função já existe. E o manipulador de exceções deve capturar apenas este erro. Como outras respostas mencionadas, é uma boa ideia converter o erro fatal em aviso simples. Outros IF NOT EXISTScomandos PostgreSQL adicionam, skipping à mensagem, portanto, para consistência, estou adicionando-os aqui também.

Aqui está o código SQL completo para simulação CREATE ROLE IF NOT EXISTScom exceção correta e propagação sqlstate:

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

Saída de teste (chamado duas vezes via DO e depois diretamente):

$ 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
fonte
2
Obrigado. Sem condições de corrida, captura de exceção restrita, envolvendo a própria mensagem do Postgres em vez de reescrever a sua própria.
Stefano Taschini
1
De fato! Esta é atualmente a única resposta correta aqui, que não sofre de condições de corrida e usa o tratamento de erro seletivo necessário. É uma pena que esta resposta tenha aparecido depois que a resposta principal (não totalmente correta) coletou mais de 100 pontos.
vog
1
Você é bem vindo! Minha solução também propaga SQLSTATE, portanto, se você estiver chamando uma instrução de outro script PL / SQL ou outra linguagem com conector SQL, receberá SQLSTATE correto.
Pali
6

Como você está no 9.x, você pode embrulhar isso em uma instrução 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$
;
um cavalo sem nome
fonte
Select deve ser `SELECT count (*) into num_users FROM pg_roles WHERE rolname = 'data_rw';` Caso contrário, não funcionará
Miro
6

Minha equipe estava enfrentando uma situação com vários bancos de dados em um servidor, dependendo de qual banco de dados você se conectou, o ROLE em questão não foi retornado por SELECT * FROM pg_catalog.pg_user, conforme proposto por @ erwin-brandstetter e @a_horse_with_no_name. O bloco condicional foi executado e atingimosrole "my_user" already exists .

Infelizmente, não temos certeza das condições exatas, mas esta solução contorna o 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$

Provavelmente, poderia ser mais específico para descartar outras exceções.

Chris Betti
fonte
3
A tabela pg_user parece incluir apenas funções que possuem LOGIN. Se uma função tem NOLOGIN, ela não aparece no pg_user, pelo menos no PostgreSQL 10.
Gregory Arenius
2

Você pode fazer isso em seu arquivo em lote, analisando a saída de:

SELECT * FROM pg_user WHERE usename = 'my_user'

e, em seguida, executando psql.exenovamente se a função não existir.

Sheva
fonte
2
A coluna "nome de usuário" não existe. Deve ser "nome de usuário".
Mouhammed Soueidane
3
"usename" é aquele que não existe. :)
Garen
1
Por favor, consulte o documento de visualização do pg_user . Não há coluna "nome de usuário" nas versões 7.4-9.6, "nome de usuário" é o correto.
Sheva
1

A mesma solução para Simular CREATE DATABASE IF NOT EXISTS para PostgreSQL? deve funcionar - envie um CREATE USER …para\gexec .

Solução alternativa de dentro do psql

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

Solução alternativa do shell

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

Veja a resposta aceita para mais detalhes.

Alexander Skwar
fonte
Sua solução ainda tem uma condição de corrida que eu descrevi em minha resposta stackoverflow.com/a/55954480/7878845 Se você executar seu script de shell em paralelo mais vezes, receberá ERROR: role "my_user" já existe
Pali