捕获受动态 sql 影响的行数?

Capture number of rows affected by dynamic sql?

我正在尝试从 plpgsql 函数中的 QUERY EXEUTE 获取 return,以便能够检查有多少行受到动态更新查询的影响。我的用例是在插入或更新到动态集 table 时将事件(带有自定义负载)添加到单独的 table。因为我的事件有自定义负载,所以我无法使用数据库触发器(例如插入前触发)。作为一个简化的例子,假设我有这个 table:

CREATE TABLE users (user_id text primary key, name text)

这是我简化的事件table:

CREATE TABLE events(event_id text primary key, payload json)

这是我的简化函数:

CREATE OR REPLACE FUNCTION my_function(_rowtype anyelement, q text, payload jsonb)
    RETURNS SETOF anyelement AS
$func$
DECLARE
    event_id text;
BEGIN
    SELECT jsonb_object_field_text (payload, 'id')::text INTO STRICT event_id;
    execute format('insert into event(event_id, payload) values (, )') using event_id, payload;
    RETURN QUERY EXECUTE format('%s', q);
END
$func$ LANGUAGE plpgsql;

我们的目标是让这项工作完全相同,就像有人在交易中创建这些一样。在插入的伪代码中:

BEGIN
insert into events(id, payload) values(, )
insert into users(columns) values(<any values>)
COMMIT

更新也类似:

BEGIN
insert into events(id, payload) values(, )
result, error := query(`update users set name = 'hello' where id = 'Not Exists Thus No Rows Modified'`);
if result.rowsAffected() == 0 {
   ROLLBACK
}
COMMIT

函数 my_function 除了一种极端情况外几乎可以正常工作:当更新实际上不影响任何行时。 例如,这有效:

select * from my_function(NULL::users, 
       'insert into users(id,name) values('u1', ''a2'') returning *',
       payload => '{"id": "e1", "custom": "s1", "field": "2019-10-12T07:20:50.52Z"}')      

正如预期的那样,在完成此操作后,用户 table 和事件 table 中的一行都已创建。 失败的是以下内容:

select * from my_function(NULL::users, 
       'update users set name = ''hello'' where user_id = ''NotExists'' returning *',
       payload => '{"id": "e2", "custom": "s3", "field": "2019-10-12T07:20:50.52Z"}')     

这里,在事件中创建了一行table(我的目标是不应该创建)。 我知道这种方法并不优雅,而且我知道这很容易受到 SQL 注入的攻击。我喜欢关于解决此问题的更好方法的建议(包括取消我们现在正在做的事情)。但是为了直接回答这个问题,我希望存储 QUERY EXECUTE 的结果,检查是否有任何行受到影响,并引发错误,这样就不会出现事件 [=39= 中的行的情况]是在用户没有真正对应的变化时创建的table。 Users table 只是一个例子,一般来说,它可以是任何动态设置 table.

A RETURN QUERY 不需要走到函数的末尾,它只说:“这个查询的结果是结果集的一部分”。

因此您可以使用 RETURN QUERY,请求 FOUND 并采取相应行动。这是为以这种方式工作而修改的函数:

CREATE OR REPLACE FUNCTION public.my_function(_rowtype anyelement, q text, payload jsonb)
 RETURNS SETOF anyelement
 LANGUAGE plpgsql
AS $function$
DECLARE
    event_id text;
BEGIN
    SELECT jsonb_object_field_text (payload, 'id')::text INTO STRICT event_id;

    RETURN QUERY EXECUTE format('%s', q);
    IF FOUND THEN
        execute format('insert into events(event_id, payload) values (, )') using event_id, payload;
    END IF;

    RETURN;
END
$function$

PD:也许您还可以使用转换表 OLD 和 NEW(从 v10 开始可用,https://www.postgresql.org/docs/10/sql-createtrigger.html)通过触发器 FOR EACH STATEMENT 解决您的问题

CREATE OR REPLACE FUNCTION my_function(_rowtype anyelement, q text, payload jsonb)
  RETURNS SETOF anyelement
  LANGUAGE plpgsql AS
$func$
BEGIN
   RETURN QUERY EXECUTE q;

   IF NOT FOUND THEN 
      RETURN;  -- nothing happened yet, we can exit silently.
      -- Or you WANT an error for this case. Then do this instead:
      -- RAISE EXCEPTION 'Query passed in parameter "q" did not affect any rows. Doing nothing!';
   END IF;
   
   INSERT INTO event(event_id, payload)
   VALUES (payload->>'id', payload);
END
$func$;

正如所评论的那样,RETURN QUERY 不会从函数中 return。 The manual:

RETURN NEXT and RETURN QUERY do not actually return from the function — they simply append zero or more rows to the function's result set. Execution then continues with the next statement in the PL/pgSQL function. As successive RETURN NEXT or RETURN QUERY commands are executed, the result set is built up. A final RETURN, which should have no argument, causes control to exit the function (or you can just let control reach the end of the function).

手册中该章的底部正好有一个针对您的情况的 code example。实际上,从我这里。起源于此:

  • FUNCTION syntax error

建议使用 GET DIAGNOSTICS 而不是更简单的 FOUNDEXECUTE 确实没有设置 FOUND 的状态。但是 RETURN QUERY 确实如此。所以继续使用更简单的 FOUND。相关:

  • Dynamic SQL (EXECUTE) as condition for IF statement

您的原件中有 format() 两次。虽然这通常对动态 SQL 非常有用,但在您的情况下却毫无用处。 EXECUTE format('%s', q)EXECUTE q 完全相同,只是增加了成本。两者都是在传递用户输入时为 SQL 注入打开的大门。

虽然事务很有可能被回滚,但从关键步骤开始,其余的稍后再做。避免浪费工作。所以我将 executing q 移动到顶部。 假设它不依赖于现在稍后插入的“有效负载”行。

另外,INSERT INTO events可以是普通的SQL。那里没有动态。不需要 format()EXECUTE.

最后,假设您的 jsonb_object_field_text (payload, 'id')::text 只是 payload->>'id' 的另一种说法。不需要额外的变量和另一个 SELECT INTO.

针对 SQL 注入的警告

将用户输入(示例中的参数 q)转换为动态执行的代码是所有注入漏洞中最直接的 SQL。我可不想那样做时被夹在内衣里。