Nome da tabela como um parâmetro de função PostgreSQL

87

Quero passar um nome de tabela como um parâmetro em uma função Postgres. Eu tentei este código:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
    BEGIN
    IF EXISTS (select * from quote_ident($1) where quote_ident($1).id=1) THEN
     return 1;
    END IF;
    return 0;
    END;
$$ LANGUAGE plpgsql;

select some_f('table_name');

E eu tenho isso:

ERROR:  syntax error at or near "."
LINE 4: ...elect * from quote_ident($1) where quote_ident($1).id=1)...
                                                             ^

********** Error **********

ERROR: syntax error at or near "."

E aqui está o erro que recebi quando mudei para isso select * from quote_ident($1) tab where tab.id=1:

ERROR:  column tab.id does not exist
LINE 1: ...T EXISTS (select * from quote_ident($1) tab where tab.id...

Provavelmente quote_ident($1)funciona, porque sem a where quote_ident($1).id=1parte que recebo 1, o que significa que algo está selecionado. Por que o primeiro pode quote_ident($1)funcionar e o segundo não ao mesmo tempo? E como isso poderia ser resolvido?

John Doe
fonte
Sei que essa pergunta é meio antiga, mas a encontrei enquanto procurava a resposta para outra questão. Sua função não poderia apenas consultar o esquema_informacional? Quer dizer, é para isso que serve de certa forma - para permitir que você consulte e veja quais objetos existem no banco de dados. Apenas uma ideia.
David S
@DavidS Obrigado por um comentário, vou tentar isso.
John Doe

Respostas:

126

Isso pode ser ainda mais simplificado e melhorado:

CREATE OR REPLACE FUNCTION some_f(_tbl regclass, OUT result integer)
    LANGUAGE plpgsql AS
$func$
BEGIN
   EXECUTE format('SELECT (EXISTS (SELECT FROM %s WHERE id = 1))::int', _tbl)
   INTO result;
END
$func$;

Chamada com nome qualificado pelo esquema (veja abaixo):

SELECT some_f('myschema.mytable');  -- would fail with quote_ident()

Ou:

SELECT some_f('"my very uncommon table name"');

Pontos principais

  • Use um OUTparâmetro para simplificar a função. Você pode selecionar diretamente o resultado do SQL dinâmico nele e pronto. Não há necessidade de variáveis ​​e códigos adicionais.

  • EXISTSfaz exatamente o que você quer. Você obtém truese a linha existe ou falsenão. Existem várias maneiras de fazer isso, EXISTSnormalmente é mais eficiente.

  • Você parece querer um número inteiro de volta, então lancei o booleanresultado de EXISTSpara integer, que produz exatamente o que você tinha. Eu retornaria booleano .

  • Eu uso o tipo de identificador de objeto regclasscomo tipo de entrada para _tbl. Isso faz tudo quote_ident(_tbl)ou format('%I', _tbl)faria, mas melhor, porque:

  • .. também evita a injeção de SQL .

  • .. falha imediatamente e de forma mais elegante se o nome da tabela for inválido / não existir / for invisível para o usuário atual. (Um regclassparâmetro é aplicável apenas para tabelas existentes .)

  • .. funciona com nomes de tabela qualificados por esquema, em que um simples quote_ident(_tbl)ou format(%I)falharia porque não podem resolver a ambigüidade. Você teria que passar e escapar nomes de esquema e tabela separadamente.

  • Ainda uso format(), porque simplifica a sintaxe (e para demonstrar como é usada), mas com em %svez de %I. Normalmente, as consultas são mais complexas, então format()ajuda mais. Para o exemplo simples, podemos simplesmente concatenar:

      EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
    
  • Não há necessidade de qualificar a idcoluna para a tabela enquanto houver apenas uma única tabela na FROMlista. Nenhuma ambigüidade possível neste exemplo. Os comandos SQL (dinâmicos) internos EXECUTEtêm um escopo separado , as variáveis ​​ou parâmetros da função não são visíveis ali - ao contrário dos comandos SQL simples no corpo da função.

Veja por que você sempre escapa da entrada do usuário para SQL dinâmico corretamente:

db <> fiddle aqui demonstrando injeção de SQL
Old sqlfiddle

Erwin Brandstetter
fonte
2
@suhprano: Claro. Experimente:DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
Erwin Brandstetter
por que% s e não% L?
Lotus
3
@Lotus: A explicação está na resposta. regclassos valores são escapados automaticamente na saída como texto. %Lestaria errado neste caso.
Erwin Brandstetter
CREATE OR REPLACE FUNCTION table_rows(_tbl regclass, OUT result integer) AS $func$ BEGIN EXECUTE 'SELECT (SELECT count(1) FROM ' || _tbl || ' )::int' INTO result; END $func$ LANGUAGE plpgsql; criar uma função de contagem de linha da tabela,select table_rows('nf_part1');
Ferris
como podemos obter todas as colunas?
Ashish
13

Se possível, não faça isso.

Essa é a resposta - é um antipadrão. Se o cliente conhece a mesa da qual deseja os dados, então SELECT FROM ThatTable. Se um banco de dados for projetado de forma que isso seja necessário, ele parece ter sido projetado de forma subotimizada. Se uma camada de acesso a dados precisa saber se existe um valor em uma tabela, é fácil compor o SQL nesse código e colocar esse código no banco de dados não é bom.

Para mim, isso parece instalar um dispositivo dentro de um elevador onde se pode digitar o número do andar desejado. Depois que o botão Go é pressionado, ele move uma mão mecânica sobre o botão correto para o andar desejado e o pressiona. Isso apresenta muitos problemas potenciais.

Atenção: não há intenção de zombaria, aqui. Meu exemplo bobo de elevador foi * o melhor dispositivo que eu poderia imaginar * para apontar problemas de forma sucinta com essa técnica. Ele adiciona uma camada inútil de indireção, movendo a escolha do nome da tabela de um espaço de chamada (usando um DSL robusto e bem compreendido, SQL) para um híbrido usando código SQL obscuro / bizarro do lado do servidor.

Essa divisão de responsabilidade por meio do movimento da lógica de construção da consulta em SQL dinâmico torna o código mais difícil de entender. Ele viola uma convenção padrão e confiável (como uma consulta SQL escolhe o que selecionar) em nome de um código personalizado repleto de potencial de erro.

Aqui estão pontos detalhados sobre alguns dos problemas potenciais com esta abordagem:

  • O SQL dinâmico oferece a possibilidade de injeção de SQL que é difícil de reconhecer no código do front-end ou apenas no código do back-end (é necessário inspecioná-los juntos para ver isso).

  • Os procedimentos e funções armazenados podem acessar recursos aos quais o proprietário da função / SP tem direitos, mas o chamador não. Pelo que entendi, sem cuidados especiais, então por padrão, quando você usa código que produz SQL dinâmico e o executa, o banco de dados executa o SQL dinâmico sob os direitos do autor da chamada. Isso significa que você não poderá usar objetos privilegiados ou terá que abri-los para todos os clientes, aumentando a área de superfície de ataque potencial para dados privilegiados. Definir a função SP / no momento da criação para sempre ser executado como um usuário específico (no SQL Server EXECUTE AS) pode resolver esse problema, mas torna as coisas mais complicadas. Isso exacerba o risco de injeção de SQL mencionado no ponto anterior, tornando o SQL dinâmico um vetor de ataque muito atraente.

  • Quando um desenvolvedor precisa entender o que o código do aplicativo está fazendo para modificá-lo ou consertar um bug, ele achará muito difícil obter a consulta SQL exata que está sendo executada. O SQL Profiler pode ser usado, mas exige privilégios especiais e pode ter efeitos negativos no desempenho dos sistemas de produção. A consulta executada pode ser registrada pelo SP, mas isso aumenta a complexidade para benefícios questionáveis ​​(exigindo acomodação de novas tabelas, eliminação de dados antigos, etc.) e não é óbvio. Na verdade, alguns aplicativos são arquitetados de forma que o desenvolvedor não tenha credenciais de banco de dados, portanto, torna-se quase impossível para ele realmente ver a consulta sendo enviada.

  • Quando ocorre um erro, como ao tentar selecionar uma tabela que não existe, você receberá uma mensagem semelhante a "nome de objeto inválido" do banco de dados. Isso vai acontecer exatamente da mesma forma, quer você esteja compondo o SQL no back-end ou no banco de dados, mas a diferença é que algum desenvolvedor pobre que está tentando solucionar o sistema precisa inserir um nível mais profundo em outra caverna abaixo daquela onde o o problema existe, para mergulhar no procedimento maravilhoso que faz tudo para tentar descobrir qual é o problema. Os logs não mostrarão "Error in GetWidget", mas sim "Error in OneProcedureToRuleThemAllRunner". Essa abstração geralmente tornará o sistema pior .

Um exemplo em pseudo-C # de troca de nomes de tabelas com base em um parâmetro:

string sql = $"SELECT * FROM {EscapeSqlIdentifier(tableName)};"
results = connection.Execute(sql);

Embora isso não elimine todos os problemas imagináveis, as falhas que descrevi com a outra técnica estão ausentes neste exemplo.

ErikE
fonte
4
Eu não concordo totalmente com isso. Digamos, você pressiona este botão "Go" e, em seguida, algumas verificações de mecanismo, se o piso existe. Funções podem ser usadas em triggers, que por sua vez podem verificar algumas condições. Essa decisão pode não ser a mais bonita, mas se o sistema já for grande o suficiente e você precisar fazer algumas correções em sua lógica, bem, essa escolha não é tão dramática, suponho.
John Doe
2
Mas considere que a ação de tentar pressionar um botão que não existe simplesmente irá gerar uma exceção, não importa como você lide com isso. Você não pode realmente apertar um botão inexistente, então não há nenhum benefício em adicionar, além de apertar o botão, uma camada para verificar se há números inexistentes, uma vez que tal entrada de número não existia antes de você criar a referida camada! A abstração é, em minha opinião, a ferramenta mais poderosa de programação. No entanto, adicionar uma camada que apenas duplica mal uma abstração existente é errado . O próprio banco de dados é uma camada de abstração que mapeia nomes para conjuntos de dados.
ErikE
3
Spot on. O objetivo do SQL é expressar o conjunto de dados que você deseja extrair. A única coisa que essa função faz é encapsular uma instrução SQL "enlatada". Dado o fato de que o identificador também é codificado, a coisa toda tem um cheiro ruim.
Nick Hristov
2
@três Até que alguém esteja na fase de domínio (veja o modelo de Dreyfus de aquisição de habilidade ) de uma habilidade, ele deve simplesmente obedecer regras como "NÃO passe nomes de tabelas em um procedimento a ser usado em SQL dinâmico". Mesmo insinuar que nem sempre é ruim é um mau conselho . Sabendo disso, o iniciante ficará tentado a usá-lo! Isso é ruim. Apenas os mestres de um tópico devem quebrar as regras, pois são os únicos com experiência para saber, em qualquer caso específico, se essa quebra de regra realmente faz sentido.
ErikE
2
@ três xícaras eu atualizei com muito mais detalhes sobre por que é uma má ideia.
ErikE
10

Dentro do código plpgsql, a instrução EXECUTE deve ser usada para consultas em que nomes de tabelas ou colunas vêm de variáveis. Além disso, a IF EXISTS (<query>)construção não é permitida quando queryé gerada dinamicamente.

Esta é sua função com os dois problemas corrigidos:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
DECLARE
 v int;
BEGIN
      EXECUTE 'select 1 FROM ' || quote_ident(param) || ' WHERE '
            || quote_ident(param) || '.id = 1' INTO v;
      IF v THEN return 1; ELSE return 0; END IF;
END;
$$ LANGUAGE plpgsql;
Daniel Vérité
fonte
Obrigado, estava fazendo o mesmo há alguns minutos quando li sua resposta. A única diferença é que tive que remover quote_ident()porque adicionou aspas extras, o que me surpreendeu um pouco, bem, porque é usado na maioria dos exemplos.
John Doe
Essas aspas extras serão necessárias se / quando o nome da tabela contiver caracteres fora de [az], ou se / quando colidir com um identificador reservado (exemplo: "grupo" como nome de tabela)
Daniel Vérité
E, a propósito, você poderia fornecer um link que prove que esse IF EXISTS <query>constructo não existe? Tenho certeza de que vi algo assim como um exemplo de código funcional.
John Doe
1
@JohnDoe: IF EXISTS (<query>) THEN ...é uma construção perfeitamente válida em plpgsql. Só não com SQL dinâmico para <query>. Eu uso isto muito. Além disso, esta função pode ser melhorada um pouco. Eu postei uma resposta.
Erwin Brandstetter
1
Desculpe, você está certo sobre if exists(<query>), é válido no caso geral. Apenas verifiquei e modifiquei a resposta de acordo.
Daniel Vérité
4

O primeiro realmente não "funciona" no sentido que você quer dizer, funciona apenas na medida em que não gera um erro.

Tente SELECT * FROM quote_ident('table_that_does_not_exist');, e você verá porque sua função retorna 1: o select está retornando uma tabela com uma coluna (nomeada quote_ident) com uma linha (a variável $1ou neste caso particular table_that_does_not_exist).

O que você deseja fazer exigirá SQL dinâmico, que é realmente o local onde as quote_*funções devem ser usadas.

Matt
fonte
Muito obrigado, Matt, table_that_does_not_existdeu o mesmo resultado, você tem razão.
John Doe
2

Se a questão era testar se a tabela está vazia ou não (id = 1), aqui está uma versão simplificada do procedimento armazenado de Erwin:

CREATE OR REPLACE FUNCTION isEmpty(tableName text, OUT zeroIfEmpty integer) AS
$func$
BEGIN
EXECUTE format('SELECT COALESCE ((SELECT 1 FROM %s LIMIT 1),0)', tableName)
INTO zeroIfEmpty;
END
$func$ LANGUAGE plpgsql;
Julien Feniou
fonte
1

Eu sei que este é um tópico antigo, mas eu o encontrei recentemente ao tentar resolver o mesmo problema - no meu caso, para alguns scripts bastante complexos.

Transformar todo o script em SQL dinâmico não é o ideal. É um trabalho tedioso e sujeito a erros, e você perde a capacidade de parametrizar: os parâmetros devem ser interpolados em constantes no SQL, com consequências ruins para o desempenho e a segurança.

Aqui está um truque simples que permite manter o SQL intacto se você só precisar selecionar em sua tabela - use SQL dinâmico para criar uma visualização temporária:

CREATE OR REPLACE FUNCTION some_f(_tbl varchar) returns integer
AS $$
BEGIN
    drop view if exists myview;
    execute format('create temporary view myview as select * from %s', _tbl);
    -- now you can reference myview in the SQL
    IF EXISTS (select * from myview where myview.id=1) THEN
     return 1;
    END IF;
    return 0;
END;
$$ language plpgsql;
Nathan Meyers
fonte
0

Se você quiser que o nome da tabela, o nome da coluna e o valor sejam passados ​​dinamicamente para funcionar como parâmetro

use este código

create or replace function total_rows(tbl_name text, column_name text, value int)
returns integer as $total$
declare
total integer;
begin
    EXECUTE format('select count(*) from %s WHERE %s = %s', tbl_name, column_name, value) INTO total;
    return total;
end;
$total$ language plpgsql;


postgres=# select total_rows('tbl_name','column_name',2); --2 is the value
Sandip Debnath
fonte
-2

Tenho a versão 9.4 do PostgreSQL e sempre uso este código:

CREATE FUNCTION add_new_table(text) RETURNS void AS
$BODY$
begin
    execute
        'CREATE TABLE ' || $1 || '(
        item_1      type,
        item_2      type
        )';
end;
$BODY$
LANGUAGE plpgsql

E depois:

SELECT add_new_table('my_table_name');

Isso funciona bem para mim.

Atenção! O exemplo acima é um daqueles que mostra "Como não fazer se quisermos manter a segurança durante a consulta ao banco de dados": P

dm3
fonte
1
Criar uma newtabela é diferente de operar com o nome de uma tabela existente. De qualquer forma, você deve escapar dos parâmetros de texto executados como código ou estará aberto à injeção de SQL.
Erwin Brandstetter
Oh, sim, erro meu. O assunto me enganou e, além disso, não o li até o fim. Normalmente no meu caso. : P Por que o código com um parâmetro de texto é exposto à injeção?
dm3
Opa, é muito perigoso. Obrigado pela resposta!
dm3