web-dev-qa-db-fra.com

Nom de la table en tant que paramètre de fonction PostgreSQL

Je veux passer un nom de table en tant que paramètre dans une fonction Postgres. J'ai essayé ce code:

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');

Et j'ai eu ceci:

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 "."

Et voici l'erreur que j'ai eu lorsque j'ai changé pour cette 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...

quote_ident($1) fonctionne probablement, car sans la partie where quote_ident($1).id=1, je reçois 1, ce qui signifie que quelque chose est sélectionné. Pourquoi le premier quote_ident($1) fonctionne-t-il et le second ne fonctionne-t-il pas en même temps? Et comment cela pourrait-il être résolu?

60
John Doe

Cela peut être encore simplifié et amélioré:

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

Appel avec nom qualifié (voir ci-dessous):

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

Ou:

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

Points majeurs

  • Utilisez un paramètre OUT pour simplifier la fonction. Vous pouvez directement sélectionner le résultat du SQL dynamique et le faire. Pas besoin de variables et de code supplémentaires.

  • EXISTS fait exactement ce que vous voulez. Vous obtenez true si la ligne existe ou false sinon. Il existe différentes façons de procéder. EXISTS est généralement le plus efficace.

  • Vous semblez vouloir récupérer integer, alors jette le résultat boolean de EXISTS à integer, ce qui donne exactement ce que vous aviez. Je retournerais boolean à la place.

  • J'utilise le type d'identifiant d'objet regclass comme type d'entrée pour _tbl. Cela fait tout quote_ident(_tbl) ou format('%I', _tbl) ferait l'affaire, mais mieux, parce que: 

    • ..il évite aussi bien SQL injection

    • .. il échoue immédiatement et plus gracieusement si le nom de la table n'est pas valide/n'existe pas/est invisible pour l'utilisateur actuel. (Un paramètre regclass s'applique uniquement aux tables existantes.)

    • .. cela fonctionne avec les noms de table qualifiés du schéma, dans lequel un quote_ident(_tbl) ou un format(%I) simple échouerait car ils ne pourraient pas résoudre l'ambiguïté. Vous devez transmettre et échapper les noms de schéma et de table séparément.

  • J'utilise toujours format() , parce que cela simplifie la syntaxe (et montre comment il est utilisé), mais avec %s au lieu de %I. Les requêtes étant généralement plus complexes, format() aide davantage. Pour l'exemple simple, nous pourrions aussi simplement concaténer:

    EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
    
  • Nul besoin de qualifier de table la colonne id alors qu'il n'y a qu'une seule table dans la liste FROM. Aucune ambiguïté possible dans cet exemple. Les commandes SQL (dynamiques) dans EXECUTE ont une étendue séparée, les variables de fonction ou les paramètres n'y sont pas visibles - contrairement aux commandes SQL simples dans le corps de la fonction. 

Testé avec PostgreSQL 9.1. format() nécessite au moins cette version.

Voici pourquoi vous toujours échappez correctement à la saisie utilisateur pour le SQL dynamique:

SQL Fiddle démontrant l'injection SQL

91

Ne fais pas ça.

C'est la réponse. C'est un anti-modèle terrible. A quoi cela sert-il? Si le client connaît la table sur laquelle il veut des données, alors SELECT FROM ThatTable! Si vous avez conçu votre base de données de la manière requise, vous l'avez probablement mal conçue. Si votre couche d'accès aux données doit savoir si une valeur existe dans une table, il est très facile de créer la partie SQL dynamique de ce code. Il n'est pas bon de l'insérer dans la base de données.

J'ai une idée: installons un appareil à l'intérieur des ascenseurs où vous pouvez taper le numéro de l'étage que vous voulez. Ensuite, lorsque vous appuyez sur "Go", il déplace une main mécanique sur le bouton correspondant au sol souhaité et l’appuie pour vous. Révolutionnaire!

Apparemment, ma réponse était trop courte en explication, donc je répare ce défaut avec plus de détails.

Je n'avais aucune intention de se moquer. Mon exemple stupide d'ascenseur était le meilleur appareil que je pouvais imaginer pour souligner succinctement les failles de la technique suggérée dans la question. Cette technique ajoute une couche d'indirection totalement inutile et déplace inutilement le choix du nom de la table d'un espace appelant utilisant un DSL (SQL) robuste et bien compris vers un hybride utilisant un code SQL côté serveur obscur/bizarre.

Cette division des responsabilités par le déplacement de la logique de construction de requête en SQL dynamique rend le code plus difficile à comprendre. Cela détruit une convention parfaitement raisonnable (comment une requête SQL choisit quoi sélectionner) dans le nom de code personnalisé semé d’erreur potentielle.

  • Le SQL dynamique offre la possibilité d'une injection SQL difficile à reconnaître dans le code frontal ou le code principal séparément (il faut les inspecter ensemble pour voir cela).

  • Les procédures et fonctions stockées peuvent accéder aux ressources pour lesquelles le propriétaire du SP/fonction dispose de droits, mais non de l'appelant. Autant que je sache, lorsque vous utilisez du code qui génère et exécute du code SQL dynamique, la base de données exécute le code SQL dynamique sous les droits de l'appelant. Cela signifie soit que vous ne pourrez plus utiliser d'objets privilégiés, soit que vous devrez les ouvrir à tous les clients, ce qui augmentera la surface d'attaque potentielle pour les données privilégiées. Définir le SP/fonction au moment de la création pour qu'il s'exécute toujours en tant qu'utilisateur particulier (dans SQL Server, EXECUTE AS) peut résoudre ce problème, mais rend les choses plus compliquées. Cela exacerbe le risque d'injection SQL mentionné dans le point précédent, en faisant du SQL dynamique un vecteur d'attaque très séduisant.

  • Lorsqu'un développeur doit comprendre ce que fait le code de l'application pour le modifier ou corriger un bogue, il aura beaucoup de difficulté à obtenir l'exécution de la requête SQL exacte. Le profileur SQL peut être utilisé, mais cela nécessite des privilèges spéciaux et peut avoir des effets négatifs sur les performances des systèmes de production. La requête exécutée peut être consignée par le SP, mais cela ne fait qu’accroître la complexité sans aucune raison (conserver de nouvelles tables, purger les anciennes données, etc.) et n’est absolument pas évidente. En fait, certaines applications sont architecturées de telle sorte que le développeur ne dispose pas des informations d'identification de la base de données. Il lui devient donc presque impossible de voir la requête soumise.

  • Lorsqu'une erreur se produit, par exemple lorsque vous essayez de sélectionner une table qui n'existe pas, vous recevez un message du type "nom d'objet non valide" de la base de données. Cela se produira exactement de la même manière, que vous composiez le code SQL dans le back-end ou dans la base de données, mais la différence est qu'un mauvais développeur qui tente de dépanner le système doit approfondir un niveau supplémentaire dans une autre cavité située en dessous de celle où le problème existe réellement, pour creuser dans la procédure miracle qui fait tout et essayer de comprendre quel est le problème. Les journaux n'indiqueront pas "Erreur dans GetWidget", mais "Erreur dans OneProcedureToRuleThemAllRunner". Cette abstraction ne fera que rendre votre système pire .

Voici un exemple bien meilleur en pseudo-C # de noms de table de commutation basés sur un paramètre:

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

Chaque défaut que j'ai mentionné avec l'autre technique est complètement absent de cet exemple.

Il n'y a aucune utilité, aucun avantage, aucune amélioration possible dans la soumission d'un nom de table à une procédure stockée.

15
ErikE

Dans le code plpgsql, l'instruction EXECUTE doit être utilisée pour les requêtes dans lesquelles les noms de tables ou les colonnes proviennent de variables. De plus, la construction IF EXISTS (<query>) n'est pas autorisée lorsque query est généré de manière dynamique.

Voici votre fonction avec les deux problèmes résolus:

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;
9
Daniel Vérité

Le premier ne "fonctionne" pas réellement dans le sens où vous voulez dire, il ne fonctionne que dans la mesure où il ne génère pas d'erreur.

Essayez SELECT * FROM quote_ident('table_that_does_not_exist'); et vous verrez pourquoi votre fonction renvoie 1: la sélection renvoie une table avec une colonne (nommée quote_ident) avec une ligne (la variable $1 ou dans ce cas particulier table_that_does_not_exist).

Ce que vous voulez faire nécessitera du SQL dynamique, qui est en réalité le lieu où les fonctions quote_* sont censées être utilisées.

3
Matt

Si la question était de vérifier si la table est vide ou non (id = 1), voici une version simplifiée du processus stocké d'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;
0
Julien Feniou

Si vous souhaitez que le nom de la table, le nom de la colonne et la valeur soient passés dynamiquement à function en tant que paramètre

utiliser ce code

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
0
Sandip Debnath