Имя таблицы как параметр функции PostgreSQL

Я хочу передать имя таблицы в качестве параметра в функции 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) работает, а вторая - не в одно и то же время? И как это можно решить?

Ответ 1

Это может быть дополнительно упрощено и улучшено:

CREATE OR REPLACE FUNCTION some_f(_tbl regclass, OUT result integer) AS
$func$
BEGIN
   EXECUTE format('SELECT (EXISTS (SELECT FROM %s WHERE id = 1))::int', _tbl)
   INTO result;
END
$func$  LANGUAGE plpgsql;

Позвоните с именем, дополненным схемой (см. ниже):

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 в теле функции.

Протестировано с PostgreSQL 9.1. format() требует как минимум эту версию.

Вот почему вы всегда корректно избегаете пользовательского ввода для динамического SQL:

db & lt;> fiddle здесь демонстрирует внедрение SQL.
Старый sqlfiddle.

Ответ 2

Не делай этого.

Это ответ. Это ужасный анти-паттерн. Какой цели это служит? Если клиент знает таблицу, из которой он хочет получить данные, тогда 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". Эта абстракция только ухудшит вашу систему.

Вот гораздо лучший пример псевдо-С# переключения имен таблиц на основе параметра:

string sql = string.Format("SELECT * FROM {0};", EscapeSqlIdentifier(tableName));
results = connection.Execute(sql);

В этом примере все недостатки, о которых я упоминал в другом методе, полностью отсутствуют.

Там просто нет цели, нет выгоды, нет возможных улучшений в представлении имени таблицы для хранимой процедуры.

Ответ 3

Внутри кода 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;

Ответ 4

Первая на самом деле не "работает" в том смысле, что вы имеете в виду, она работает только в том случае, если она не генерирует ошибку.

Попробуйте SELECT * FROM quote_ident('table_that_does_not_exist');, и вы увидите, почему ваша функция возвращает 1: выбор возвращает таблицу с одним столбцом (с именем quote_ident) с одной строкой (переменная $1 или в этом конкретном случае table_that_does_not_exist).

Для чего вам понадобится динамический SQL, который на самом деле является местом, в котором предполагается использовать функции quote_*.

Ответ 5

Если вопрос состоял в том, чтобы проверить, является ли таблица пустой или нет (id = 1), вот упрощенная версия Erwin, хранящая proc:

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;

Ответ 6

Если вы хотите, чтобы имя таблицы, имя столбца и значение динамически передавались для функции в качестве параметра

используйте этот код

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

Ответ 7

У меня есть версия PostgreSQL версии 9.4, и я всегда использую этот код:

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