Ett varningsord :denna stil med dynamisk SQL i SECURITY DEFINER
funktioner kan vara eleganta och bekväma. Men överanvänd det inte. Kapsla inte flera nivåer av funktioner på detta sätt:
- Stilen är mycket mer felbenägen än vanlig SQL.
- Kontextväxeln med
SECURITY DEFINER
har en prislapp. - Dynamisk SQL med
EXECUTE
kan inte spara och återanvända frågeplaner. - Ingen "function inlining".
- Och jag skulle helst inte använda det för stora frågor på stora bord alls. Den extra sofistikeringen kan vara en prestandabarriär. Som:parallellism är inaktiverat för frågeplaner på detta sätt.
Som sagt, din funktion ser bra ut, jag ser inget sätt för SQL-injektion. format() har visat sig vara bra för att sammanfoga och citera värden och identifierare för dynamisk SQL. Tvärtom kan du ta bort en del redundans för att göra det billigare.
Funktionsparametrar offset__i
och limit__i
är integer
. SQL-injektion är omöjlig genom heltal, det finns egentligen inget behov av att citera dem (även om SQL tillåter strängkonstanter i citattecken för LIMIT
och OFFSET
). Så bara:
format(' OFFSET %s LIMIT %s', offset__i, limit__i)
Dessutom, efter att ha verifierat att varje key__v
är bland dina juridiska kolumnnamn - och även om de alla är lagliga kolumnnamn utan citattecken - finns det ingen anledning att köra den genom %I
. Kan bara vara %s
Jag använder hellre text
istället för varchar
. Inte en stor sak, men text
är den "föredragna" strängtypen.
Relaterat:
- Formatspecifikation för heltalsvariabler i format() för EXECUTE?
- Funktion för att returnera dynamisk uppsättning kolumner för given tabell
COST 1
verkar för lågt. Handboken:
Lämna COST
om du inte vet bättre till standard 100
.
Single set-baserad operation istället för all looping
Hela loopingen kan ersättas med en enda SELECT
påstående. Bör vara märkbart snabbare. Uppdrag är jämförelsevis dyra i PL/pgSQL. Så här:
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<>fiol här
Kortare, snabbare och fortfarande säker mot SQLi.
Citat läggs endast till när det är nödvändigt för syntax eller för att försvara sig mot SQL-injektion. Brinner endast till filtervärden. Kolumnnamn och operatörer verifieras mot den fasta listan över tillåtna alternativ.
Indata är json
istället för jsonb
. Ordningen på objekten bevaras i json
, så att du kan bestämma sekvensen av kolumner i SELECT
lista (vilket är meningsfullt) och WHERE
förhållanden (som är rent kosmetiska). Funktionen observerar båda nu.
Utdata _result
är fortfarande jsonb
. Använda en OUT
parameter istället för variabeln. Det är helt valfritt, bara för bekvämlighets skull. (Ingen uttrycklig RETURN
uttalande krävs.)
Notera den strategiska användningen av concat()
för att tyst ignorera NULL och sammanlänkningsoperatorn ||
så att NULL gör den sammanlänkade strängen NULL. På det här sättet, FROM
, WHERE
, LIMIT
och OFFSET
sätts endast in där det behövs. En SELECT
statement fungerar utan någon av dessa. En tom SELECT
list (också laglig, men jag antar att det är oönskat) resulterar i ett syntaxfel. Allt avsett.
Med format()
endast för WHERE
filter, för bekvämlighet och för att citera värden. Se:
Funktionen är inte STRICT
längre. _limit
och _offset
har standardvärdet NULL
, så bara den första parametern _options
krävs. _limit
och _offset
kan vara NULL eller utelämnas, sedan tas var och en från satsen.
Använder text
istället för varchar
.
Gjorde konstantvariabler faktiskt CONSTANT
(mest för dokumentation).
Annat än att funktionen gör vad ditt original gör.