Едно предупреждение :този стил с динамичен SQL в SECURITY DEFINER
функциите могат да бъдат елегантни и удобни. Но не прекалявайте с него. Не влагайте множество нива на функции по този начин:
- Стилът е много по-податлив на грешки от обикновения SQL.
- Превключването на контекста с
SECURITY DEFINER
има етикет с цена. - Динамичен SQL с
EXECUTE
не може да записва и използва повторно планове за заявки. - Без „вграждане на функции“.
- И аз предпочитам изобщо да не го използвам за големи заявки на големи маси. Добавената изтънченост може да бъде бариера за производителността. Като:паралелизмът е деактивиран за планове на заявки по този начин.
Въпреки това функцията ви изглежда добре, не виждам начин за SQL инжекция. format() е доказано добър за свързване и цитиране на стойности и идентификатори за динамичен SQL. Напротив, можете да премахнете някои излишъци, за да стане по-евтино.
Функционални параметри offset__i
и limit__i
са integer
. SQL инжектирането е невъзможно чрез цели числа, наистина няма нужда да ги поставяте в кавички (въпреки че SQL позволява константи на низове в кавички за LIMIT
и OFFSET
). Така че просто:
format(' OFFSET %s LIMIT %s', offset__i, limit__i)
Освен това, след като проверите, че всеки key__v
е сред законните ви имена на колони - и въпреки че всички те са законни имена на колони без кавички - няма нужда да го пускате през %I
. Може да бъде само %s
Предпочитам да използвам text
вместо varchar
. Не е голяма работа, но text
е "предпочитаният" тип низ.
Свързани:
- Спецификатор на формат за целочислени променливи във format() за EXECUTE?
- Функция за връщане на динамичен набор от колони за дадена таблица
COST 1
изглежда твърде ниско. Ръководството:
Освен ако не знаете по-добре, оставете COST
по подразбиране 100
.
Единствена операция, базирана на набор, вместо целия цикъл
Целият цикъл може да бъде заменен с един SELECT
изявление. Трябва да е значително по-бързо. Присвояванията са сравнително скъпи в PL/pgSQL. Като това:
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 тук
По-кратък, по-бърз и все още безопасен срещу SQLi.
Кавичките се добавят само когато е необходимо за синтаксис или за защита срещу SQL инжектиране. Изгаря само до филтрирани стойности. Имената на колоните и операторите се проверяват спрямо фиксирания списък с разрешени опции.
Входът е json
вместо jsonb
. Редът на обектите се запазва в json
, така че можете да определите последователността на колоните в SELECT
списък (което е смислено) и WHERE
условия (което е чисто козметично). Функцията наблюдава и двете сега.
Изход _result
все още е jsonb
. Използване на OUT
параметър вместо променливата. Това е напълно незадължително, само за удобство. (Без изрично RETURN
изисква се изявление.)
Обърнете внимание на стратегическото използване на concat()
за тихо игнориране на NULL и оператора за конкатенация ||
така че NULL прави свързания низ NULL. По този начин, FROM
, WHERE
, LIMIT
и OFFSET
се поставят само където е необходимо. A SELECT
изявлението работи без нито едно от тях. Празен SELECT
списък (също легален, но предполагам нежелан) води до синтактична грешка. Всичко по предназначение.
Използване на format()
само за WHERE
филтри, за удобство и за котиране на стойности. Вижте:
Функцията не е STRICT
вече. _limit
и _offset
имат стойност по подразбиране NULL
, така че само първият параметър _options
изисква се. _limit
и _offset
може да бъде NULL или пропуснато, след което всеки се премахва от оператора.
Използване на text
вместо varchar
.
Направи постоянни променливи всъщност CONSTANT
(най-вече за документация).
Освен това функцията прави това, което вашият оригинал прави.