表名作为PostgreSQL函数参数

时间:2012-05-22 15:55:13

标签: function postgresql plpgsql dynamic-sql identifier

我想在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)可以工作而第二个不能同时工作?怎么能解决这个问题呢?

8 个答案:

答案 0 :(得分:96)

这可以进一步简化和改进:

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,这样可以得到您所拥有的。我会返回boolean

  • 我使用对象标识符类型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列进行表格限定。在这个例子中没有歧义。 (动态)EXECUTE中的SQL命令具有单独的范围,函数变量或参数在那里不可见 - 而不是函数体中的纯SQL命令。

使用PostgreSQL 9.1进行测试。 format()至少需要该版本。

这就是为什么总是正确地转义动态SQL的用户输入的原因:

SQL Fiddle demonstrating SQL injection

答案 1 :(得分:14)

不要这样做。

这就是答案。这是一种可怕的反模式。它有什么用途?如果客户端知道它想要数据的表,那么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 = string.Format("SELECT * FROM {0};", EscapeSqlIdentifier(tableName));
results = connection.Execute(sql);

我在这个例子中完全没有提到我用其他技术提到的每一个缺陷。

向存储过程提交表名没有任何目的,没有好处,没有可能的改进。

答案 2 :(得分:9)

在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;

答案 3 :(得分:3)

第一个实际上并不是“工作”,你的意思是,它只能在不产生错误的情况下起作用。

尝试SELECT * FROM quote_ident('table_that_does_not_exist');,您将看到函数返回1的原因:select返回一个包含一行(名为quote_ident)的表,其中包含一行(变量$1或者这个特殊情况table_that_does_not_exist)。

您想要做的是需要动态SQL,这实际上是quote_*函数的用途。

答案 4 :(得分:1)

如果问题是测试表是否为空(id = 1),这里是Erwin存储过程的简化版本:

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;

答案 5 :(得分:1)

我知道这是一个旧线程,但是最近在尝试解决相同问题时遇到了这种情况-就我而言,对于一些相当复杂的脚本。

将整个脚本转换为动态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;

答案 6 :(得分:0)

如果要动态传递表名,列名和值以用作参数

使用此代码

<a href="#">Lønudvikling</a>

答案 7 :(得分:-2)

我有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