varování :tento styl s dynamickým SQL v SECURITY DEFINER
funkce mohou být elegantní a pohodlné. Ale nepřehánějte to. Nevnořujte více úrovní funkcí tímto způsobem:
- Styl je mnohem náchylnější k chybám než prostý SQL.
- Přepínání kontextu pomocí
SECURITY DEFINER
má cenovku. - Dynamické SQL s funkcí
EXECUTE
nelze uložit a znovu použít plány dotazů. - Žádné „vkládání funkcí“.
- A raději bych to vůbec nepoužíval pro velké dotazy na velkých stolech. Přidaná sofistikovanost může být překážkou výkonu. Jako:paralelismus je tímto způsobem zakázán pro plány dotazů.
To znamená, že vaše funkce vypadá dobře, nevidím žádný způsob pro SQL injection. format() je dobré zřetězit a citovat hodnoty a identifikátory pro dynamické SQL. Naopak, můžete odstranit určitou nadbytečnost, abyste to zlevnili.
Parametry funkce offset__i
a limit__i
jsou integer
. Injekce SQL je nemožná přes celá čísla, opravdu není potřeba je citovat (i když SQL umožňuje řetězcové konstanty v uvozovkách pro LIMIT
a OFFSET
). Takže jen:
format(' OFFSET %s LIMIT %s', offset__i, limit__i)
Také po ověření, že každý key__v
je mezi vašimi legálními názvy sloupců – a přestože se jedná o legální názvy sloupců bez uvozovek – není nutné jej spouštět přes %I
. Může být pouze %s
Raději bych použil text
místo varchar
. Není to velký problém, ale text
je "preferovaný" typ řetězce.
Související:
- Specifikátor formátu pro celočíselné proměnné ve format() pro EXECUTE?
- Funkce pro vrácení dynamické sady sloupců pro danou tabulku
COST 1
zdá se příliš nízké. Příručka:
Pokud nevíte lépe, ponechte COST
na výchozí 100
.
Operace založená na jedné množině namísto všech smyček
Celá smyčka může být nahrazena jediným SELECT
tvrzení. Měl by být znatelně rychlejší. Úkoly jsou v PL/pgSQL poměrně drahé. Takhle:
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<>fiddle zde
Kratší, rychlejší a stále bezpečný proti SQLi.
Uvozovky se přidávají pouze tam, kde je to nutné pro syntaxi nebo pro obranu proti SQL injection. Spálí se pouze pro filtrování hodnot. Názvy sloupců a operátory jsou ověřeny podle pevně nastaveného seznamu povolených možností.
Vstup je json
místo jsonb
. Pořadí objektů je zachováno v json
, takže můžete určit pořadí sloupců v SELECT
seznam (což má smysl) a WHERE
podmínky (což je čistě kosmetické). Funkce nyní pozoruje obojí.
Výstup _result
je stále jsonb
. Pomocí OUT
parametr místo proměnné. To je zcela volitelné, jen pro pohodlí. (Žádné explicitní RETURN
vyžadováno prohlášení.)
Všimněte si strategického použití concat()
k tichému ignorování NULL a operátoru zřetězení ||
takže NULL udělá zřetězený řetězec NULL. Tímto způsobem FROM
, WHERE
, LIMIT
a OFFSET
se vkládají pouze tam, kde je to potřeba. A SELECT
příkaz funguje bez kteréhokoli z nich. Prázdný SELECT
seznam (také legální, ale předpokládám, že nechtěný) vede k chybě syntaxe. Vše zamýšleno.
Pomocí format()
pouze pro WHERE
filtry, pro pohodlí a uvádět hodnoty. Viz:
Funkce není STRICT
už _limit
a _offset
mají výchozí hodnotu NULL
, takže pouze první parametr _options
je požadováno. _limit
a _offset
může být NULL nebo vynechán, pak je každý z příkazu odstraněn.
Pomocí text
místo varchar
.
Vytvořené konstantní proměnné ve skutečnosti CONSTANT
(většinou pro dokumentaci).
Kromě toho tato funkce dělá to, co dělá váš originál.