Я хочу передать имя таблицы в качестве параметра функции Postgres. Я пробовал этот код:
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');
И я получил вот что:
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 "."
И вот ошибка, которую я получил при изменении на это 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)
работает, потому что без той where quote_ident($1).id=1
детали я получаю 1
, а значит, что-то выделено. Почему первый может quote_ident($1)
работать, а второй нет одновременно? И как это можно было решить?
function
postgresql
plpgsql
dynamic-sql
identifier
Джон Доу
источник
источник
Ответы:
Это можно еще больше упростить и улучшить:
CREATE OR REPLACE FUNCTION some_f(_tbl regclass, OUT result integer) LANGUAGE plpgsql AS $func$ BEGIN EXECUTE format('SELECT (EXISTS (SELECT FROM %s WHERE id = 1))::int', _tbl) INTO result; END $func$;
Вызов с именем, указанным в схеме (см. Ниже):
SELECT some_f('myschema.mytable'); -- would fail with quote_ident()
Или же:
SELECT some_f('"my very uncommon table name"');
Основные моменты
Используйте
OUT
параметр, чтобы упростить функцию. Вы можете напрямую выбрать результат динамического SQL в нем и готово. Нет необходимости в дополнительных переменных и коде.EXISTS
делает именно то, что вы хотите. Вы получаете, существуетtrue
ли строка илиfalse
нет. Есть разные способы сделать это,EXISTS
как правило, наиболее эффективно.Кажется, вам нужно вернуть целое число , поэтому я привел
boolean
результат отEXISTS
кinteger
, что дает именно то, что было у вас. Вместо этого я бы вернул логическое значение .Я использую тип идентификатора объекта в
regclass
качестве типа ввода для_tbl
. Это делает всеquote_ident(_tbl)
илиformat('%I', _tbl)
подойдет, но лучше, потому что:.. он также предотвращает внедрение SQL .
.. он терпит неудачу немедленно и более изящно, если имя таблицы недействительно / не существует / невидимо для текущего пользователя. (А
regclass
Параметр применим только к существующим таблицам.).. он работает с именами таблиц с указанием схемы, где простой
quote_ident(_tbl)
или неformat(%I)
будет работать, потому что они не могут разрешить двусмысленность. Вам нужно будет передавать и экранировать имена схемы и таблицы отдельно.Я до сих пор использую
format()
, потому что он упрощает синтаксис (и демонстрирует, как он используется), но%s
вместо%I
. Обычно запросы более сложные, поэтомуformat()
помогает больше. Для простого примера мы могли бы просто объединить:EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
Нет необходимости уточнять
id
столбец, пока вFROM
списке всего одна таблица . В этом примере нет двусмысленности. (Динамические) команды SQL внутриEXECUTE
имеют отдельную область видимости , переменные функции или параметры там не видны - в отличие от простых команд SQL в теле функции.Вот почему ты всегда правильно избегаете пользовательского ввода для динамического SQL:
дб <> скрипку здесь демонстрирует инъекции SQL
Старый sqlfiddle
источник
DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
regclass
значения экранируются автоматически при выводе в виде текста.%L
было бы неправильно в этом случае.CREATE OR REPLACE FUNCTION table_rows(_tbl regclass, OUT result integer) AS $func$ BEGIN EXECUTE 'SELECT (SELECT count(1) FROM ' || _tbl || ' )::int' INTO result; END $func$ LANGUAGE plpgsql;
создать функцию подсчета строк таблицы,select table_rows('nf_part1');
По возможности не делайте этого.
Это ответ - это антипаттерн. Если клиент знает таблицу, из которой ему нужны данные, тогда
SELECT FROM ThatTable
. Если база данных спроектирована таким образом, чтобы это требовалось, кажется, что она спроектирована неоптимально. Если слою доступа к данным необходимо знать, существует ли значение в таблице, легко составить SQL в этом коде, и помещать этот код в базу данных нецелесообразно.Для меня это похоже на установку устройства внутри лифта, где можно ввести номер желаемого этажа. После нажатия кнопки Go он перемещает механическую руку к нужной кнопке для нужного этажа и нажимает ее. Это создает множество потенциальных проблем.
Обратите внимание: здесь нет намерения насмехаться. Мой глупый пример с лифтом был * самым лучшим устройством, которое я мог представить * для лаконичного указания на проблемы с этой техникой. Он добавляет бесполезный уровень косвенного обращения, перемещая выбор имени таблицы из пространства вызывающей стороны (с использованием надежного и хорошо понятного DSL, SQL) в гибрид с использованием непонятного / причудливого серверного кода SQL.
Такое разделение ответственности за счет перемещения логики построения запроса в динамический SQL затрудняет понимание кода. Это нарушает стандартное и надежное соглашение (как SQL-запрос выбирает, что выбрать) в названии настраиваемого кода чревато ошибкой.
Вот подробные сведения о некоторых потенциальных проблемах этого подхода:
Динамический SQL предлагает возможность SQL-инъекции, которую трудно распознать в коде внешнего интерфейса или только в коде серверной части (чтобы увидеть это, нужно изучить их вместе).
Хранимые процедуры и функции могут получать доступ к ресурсам, на которые владелец SP / функции имеет права, а вызывающий - нет. Насколько я понимаю, без особой осторожности, то по умолчанию, когда вы используете код, который производит динамический SQL и запускает его, база данных выполняет динамический SQL под правами вызывающего. Это означает, что вы либо вообще не сможете использовать привилегированные объекты, либо вам придется открыть их для всех клиентов, увеличивая поверхность потенциальной атаки на привилегированные данные. Установка SP / функции во время создания на постоянный запуск от имени конкретного пользователя (в SQL Server
EXECUTE AS
) может решить эту проблему, но усложняет задачу. Это усугубляет риск SQL-инъекции, упомянутый в предыдущем пункте, делая динамический SQL очень привлекательным вектором атаки.Когда разработчик должен понять, что делает код приложения, чтобы изменить его или исправить ошибку, ему будет очень трудно получить точный выполняемый SQL-запрос. Можно использовать профилировщик SQL, но это требует особых привилегий и может отрицательно сказаться на производительности производственных систем. Выполненный запрос может регистрироваться SP, но это увеличивает сложность с сомнительной выгодой (требующей размещения новых таблиц, очистки старых данных и т. Д.) И совершенно неочевидно. Фактически, некоторые приложения спроектированы таким образом, что разработчик не имеет учетных данных базы данных, поэтому для него становится практически невозможным фактически увидеть отправляемый запрос.
При возникновении ошибки, например, при попытке выбрать несуществующую таблицу, вы получите сообщение в строке «недопустимое имя объекта» из базы данных. Это будет происходить точно так же, независимо от того, составляете ли вы SQL в серверной части или в базе данных, но разница в том, что какой-то плохой разработчик, пытающийся устранить неполадки в системе, должен углубиться на один уровень глубже в еще одну пещеру ниже той, где находится Проблема существует, чтобы покопаться в чудо-процедуре, которая делает все, чтобы попытаться выяснить, в чем проблема. В журналах не будет отображаться «Ошибка в GetWidget», будет отображаться «Ошибка в OneProcedureToRuleThemAllRunner». Эта абстракция, как правило, ухудшает систему .
Пример на псевдо-C # переключения имен таблиц на основе параметра:
string sql = $"SELECT * FROM {EscapeSqlIdentifier(tableName)};" results = connection.Execute(sql);
Хотя это не устраняет все возможные проблемы, которые можно вообразить, недостатки, которые я обозначил с помощью другой техники, отсутствуют в этом примере.
источник
Внутри кода plpgsql оператор EXECUTE должен использоваться для запросов, в которых имена таблиц или столбцы берутся из переменных. Также
IF EXISTS (<query>)
конструкция не допускается, когдаquery
она создается динамически.Вот ваша функция с исправленными обеими проблемами:
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;
источник
quote_ident()
потому что он добавил лишние кавычки, что меня немного удивило, потому что оно используется в большинстве примеров.IF EXISTS <query>
конструкции не существует? Я почти уверен, что видел что-то подобное как рабочий образец кода.IF EXISTS (<query>) THEN ...
вполне допустимая конструкция в plpgsql. Только не с динамическим SQL для<query>
. Я часто им пользуюсь. Кроме того, эту функцию можно немного улучшить. Я отправил ответ.if exists(<query>)
, в общем случае это действительно так. Просто проверил и соответствующим образом изменил ответ.Первый на самом деле не «работает» в том смысле, в котором вы имеете в виду, он работает только постольку, поскольку он не вызывает ошибки.
Попробуйте
SELECT * FROM quote_ident('table_that_does_not_exist');
, и вы увидите, почему ваша функция возвращает 1: выборка возвращает таблицу с одним столбцом (с именемquote_ident
) с одной строкой (переменная$1
или в этом конкретном случаеtable_that_does_not_exist
).То, что вы хотите сделать, потребует динамического SQL, который на самом деле является местом, где
quote_*
должны использоваться функции.источник
table_that_does_not_exist
дал такой же результат, ты прав.Если вопрос заключался в том, чтобы проверить, пуста ли таблица или нет (id = 1), вот упрощенная версия хранимой процедуры Эрвина:
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;
источник
Я знаю, что это старый поток, но недавно я столкнулся с ним, пытаясь решить ту же проблему - в моем случае для некоторых довольно сложных сценариев.
Превращение всего сценария в динамический SQL - не лучший вариант. Это утомительная и подверженная ошибкам работа, и вы теряете возможность параметризации: параметры должны быть интерполированы в константы в SQL, что плохо сказывается на производительности и безопасности.
Вот простой трюк, который позволяет сохранить SQL в неизменном виде, если вам нужно только выбрать из таблицы - используйте динамический SQL для создания временного представления:
CREATE OR REPLACE FUNCTION some_f(_tbl varchar) returns integer AS $$ BEGIN drop view if exists myview; execute format('create temporary view myview as select * from %s', _tbl); -- now you can reference myview in the SQL IF EXISTS (select * from myview where myview.id=1) THEN return 1; END IF; return 0; END; $$ language plpgsql;
источник
Если вы хотите, чтобы имя таблицы, имя столбца и значение динамически передавались для работы в качестве параметра
используйте этот код
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
источник
У меня 9.4 версия PostgreSQL, и я всегда использую этот код:
CREATE FUNCTION add_new_table(text) RETURNS void AS $BODY$ begin execute 'CREATE TABLE ' || $1 || '( item_1 type, item_2 type )'; end; $BODY$ LANGUAGE plpgsql
А потом:
SELECT add_new_table('my_table_name');
У меня это работает хорошо.
Внимание! Приведенный выше пример является одним из тех, которые показывают «Как этого не сделать, если мы хотим сохранить безопасность при запросе к базе данных»: P
источник
new
таблицы отличается от работы с именем существующей таблицы. В любом случае вы должны избегать текстовых параметров, выполняемых как код, или вы открыты для SQL-инъекции.