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=1
parte 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?
function
postgresql
plpgsql
dynamic-sql
identifier
John Doe
fonte
fonte
Respostas:
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
OUT
parâ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.EXISTS
faz exatamente o que você quer. Você obtémtrue
se a linha existe oufalse
não. Existem várias maneiras de fazer isso,EXISTS
normalmente é mais eficiente.Você parece querer um número inteiro de volta, então lancei o
boolean
resultado deEXISTS
parainteger
, que produz exatamente o que você tinha. Eu retornaria booleano .Eu uso o tipo de identificador de objeto
regclass
como tipo de entrada para_tbl
. Isso faz tudoquote_ident(_tbl)
ouformat('%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
regclass
parâmetro é aplicável apenas para tabelas existentes .).. funciona com nomes de tabela qualificados por esquema, em que um simples
quote_ident(_tbl)
ouformat(%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%s
vez de%I
. Normalmente, as consultas são mais complexas, entãoformat()
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
id
coluna para a tabela enquanto houver apenas uma única tabela naFROM
lista. Nenhuma ambigüidade possível neste exemplo. Os comandos SQL (dinâmicos) internosEXECUTE
tê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
fonte
DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
regclass
os valores são escapados automaticamente na saída como texto.%L
estaria errado neste caso.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');
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.
fonte
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 quandoquery
é 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;
fonte
quote_ident()
porque adicionou aspas extras, o que me surpreendeu um pouco, bem, porque é usado na maioria dos exemplos.IF EXISTS <query>
constructo não existe? Tenho certeza de que vi algo assim como um exemplo de código funcional.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.if exists(<query>)
, é válido no caso geral. Apenas verifiquei e modifiquei a resposta de acordo.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 (nomeadaquote_ident
) com uma linha (a variável$1
ou neste caso particulartable_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.fonte
table_that_does_not_exist
deu o mesmo resultado, você tem razão.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;
fonte
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;
fonte
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
fonte
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
fonte
new
tabela é 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.