Un avertissement :ce style avec SQL dynamique dans SECURITY DEFINER
les fonctions peuvent être élégantes et pratiques. Mais n'en abusez pas. N'imbriquez pas plusieurs niveaux de fonctions de cette manière :
- Le style est beaucoup plus sujet aux erreurs que le SQL ordinaire.
- Le changement de contexte avec
SECURITY DEFINER
a un prix. - SQL dynamique avec
EXECUTE
ne peut pas enregistrer et réutiliser les plans de requête. - Pas de "fonction inlining".
- Et je préfère ne pas l'utiliser du tout pour de grosses requêtes sur de grandes tables. La sophistication supplémentaire peut être un obstacle aux performances. Comme :le parallélisme est désactivé pour les plans de requête de cette façon.
Cela dit, votre fonction a l'air bien, je ne vois aucun moyen d'injection SQL. format() s'est avéré efficace pour concaténer et citer des valeurs et des identifiants pour le SQL dynamique. Au contraire, vous pouvez supprimer certaines redondances pour le rendre moins cher.
Paramètres de la fonction offset__i
et limit__i
sont integer
. L'injection SQL est impossible via des nombres entiers, il n'est vraiment pas nécessaire de les citer (même si SQL autorise les constantes de chaîne entre guillemets pour LIMIT
et OFFSET
). Donc juste :
format(' OFFSET %s LIMIT %s', offset__i, limit__i)
Aussi, après avoir vérifié que chaque key__v
fait partie de vos noms de colonne légaux - et bien que ce soient tous des noms de colonne légaux et sans guillemets - il n'est pas nécessaire de l'exécuter via %I
. Peut être simplement %s
Je préfère utiliser text
au lieu de varchar
. Ce n'est pas grave, mais text
est le type de chaîne "préféré".
Connexe :
- Spécificateur de format pour les variables entières dans format() pour EXECUTE ?
- Fonction pour renvoyer un ensemble dynamique de colonnes pour une table donnée
COST 1
semble trop faible. Le manuel :
Sauf si vous savez mieux, laissez COST
à sa valeur par défaut 100
.
Opération basée sur un seul ensemble au lieu de toutes les boucles
L'ensemble de la boucle peut être remplacé par un seul SELECT
déclaration. Devrait être sensiblement plus rapide. Les affectations sont relativement chères en PL/pgSQL. Comme ceci :
CREATE OR REPLACE FUNCTION goods__list_json (_options json, _limit int = NULL, _offset int = NULL, OUT _result jsonb)
RETURNS jsonb
LANGUAGE plpgsql SECURITY DEFINER AS
$func$
DECLARE
_tbl CONSTANT text := 'public.goods_full';
_cols CONSTANT text[] := '{id, id__category, category, name, barcode, price, stock, sale, purchase}';
_oper CONSTANT text[] := '{<, >, <=, >=, =, <>, LIKE, "NOT LIKE", ILIKE, "NOT ILIKE", BETWEEN, "NOT BETWEEN"}';
_sql text;
BEGIN
SELECT concat('SELECT jsonb_agg(t) FROM ('
, 'SELECT ' || string_agg(t.col, ', ' ORDER BY ord) FILTER (WHERE t.arr->>0 = 'true')
-- ORDER BY to preserve order of objects in input
, ' FROM ' || _tbl
, ' WHERE ' || string_agg (
CASE WHEN (t.arr->>1)::int BETWEEN 1 AND 10 THEN
format('%s %s %L' , t.col, _oper[(arr->>1)::int], t.arr->>2)
WHEN (t.arr->>1)::int BETWEEN 11 AND 12 THEN
format('%s %s %L AND %L', t.col, _oper[(arr->>1)::int], t.arr->>2, t.arr->>3)
-- ELSE NULL -- = default - or raise exception for illegal operator index?
END
, ' AND ' ORDER BY ord) -- ORDER BY only cosmetic
, ' OFFSET ' || _offset -- SQLi-safe, no quotes required
, ' LIMIT ' || _limit -- SQLi-safe, no quotes required
, ') t'
)
FROM json_each(_options) WITH ORDINALITY t(col, arr, ord)
WHERE t.col = ANY(_cols) -- only allowed column names - or raise exception for illegal column?
INTO _sql;
IF _sql IS NULL THEN
RAISE EXCEPTION 'Invalid input resulted in empty SQL string! Input: %', _options;
END IF;
RAISE NOTICE 'SQL: %', _sql;
EXECUTE _sql INTO _result;
END
$func$;
db<>violon ici
Plus court, plus rapide et toujours sûr contre SQLi.
Les guillemets ne sont ajoutés que lorsque cela est nécessaire pour la syntaxe ou pour se défendre contre l'injection SQL. Brûle jusqu'aux valeurs de filtre uniquement. Les noms de colonne et les opérateurs sont vérifiés par rapport à la liste câblée des options autorisées.
L'entrée est json
au lieu de jsonb
. L'ordre des objets est conservé dans json
, afin que vous puissiez déterminer la séquence des colonnes dans le SELECT
list (qui a du sens) et WHERE
conditions (ce qui est purement cosmétique). La fonction observe les deux maintenant.
Sortie _result
est toujours jsonb
. Utiliser un OUT
paramètre au lieu de la variable. C'est totalement facultatif, juste pour plus de commodité. (Pas de RETURN
explicite déclaration requise.)
Notez l'utilisation stratégique de concat()
pour ignorer silencieusement NULL et l'opérateur de concaténation ||
de sorte que NULL rend la chaîne concaténée NULL. De cette façon, FROM
, WHERE
, LIMIT
, et OFFSET
ne sont insérés que là où c'est nécessaire. Un SELECT
l'instruction fonctionne sans l'un ou l'autre. Un SELECT
vide list (également légal, mais je suppose indésirable) entraîne une erreur de syntaxe. Tout est prévu.
Utilisation de format()
uniquement pour WHERE
filtres, pour plus de commodité et pour citer des valeurs. Voir :
La fonction n'est pas STRICT
plus. _limit
et _offset
avoir la valeur par défaut NULL
, donc seul le premier paramètre _options
est requis. _limit
et _offset
peut être NULL ou omis, alors chacun est supprimé de l'instruction.
Utilisation de text
au lieu de varchar
.
Fait des variables constantes en fait CONSTANT
(principalement pour la documentation).
À part cela, la fonction fait ce que fait votre original.