动态查询中的条件WHERE表达式

时间:2016-06-17 09:49:47

标签: sql postgresql plpgsql

我有以下功能

CREATE OR REPLACE FUNCTION match_custom_filter(filters text[], id text)
        RETURNS boolean LANGUAGE plpgsql as $$
        DECLARE
            r boolean;
        BEGIN
            execute format(
                'SELECT 1 FROM trackings t LEFT JOIN visitors v ON v.id = t.visitor_id
                WHERE v.id = ''%s'' AND %s',
                id,
                array_to_string(filters, ') AND ('))
            into r;
            RETURN r;
        END $$;

select v.*, array_agg(g.name) as groups from visitors v join groups g on match_custom_filter(g.formatted_custom_filters, v.id)
where v.id = 'cov4pisw00000sjctfyvwq126'
group by v.id

当过滤器不为空时,这可以正常工作。但是过滤器也可能是空的,在这种情况下,我将有一个没有右侧的悬空AND。

错误:

ERROR:  syntax error at end of input
LINE 2: ...               WHERE v.id = 'cov4pisw00000sjctfyvwq126' AND 
                                                                       ^
QUERY:  SELECT 1 FROM trackings t LEFT JOIN visitors v ON v.id = t.visitor_id
                WHERE v.id = 'cov4pisw00000sjctfyvwq126' AND 
CONTEXT:  PL/pgSQL function match_custom_filter(text[],text) line 5 at EXECUTE statement

处理此问题的最佳方式是什么?

更新:

基于JSONB过滤器对象数组生成字符串过滤器数组的示例

def build_condition(%{"filter" => filter, "field" => field, "value" => value}) when field in @default_values do
    case filter do
      "greater_than"      -> "#{field} > #{value}"
      "less_than"         -> "#{field} < #{value}"
      "is"                -> "#{field} = '#{value}'"
      "is_not"            -> "#{field} <> '#{value}'" 
      ..

2 个答案:

答案 0 :(得分:3)

首先是警告。你在这里做的是给你in-stored-proc sql注入。我强烈建议您重新考虑,以便正确参数化。

现在,说过这个,显而易见的选择是声明一个文本变量,然后对其进行预处理。

DECLARE块中添加:

 filterstring text;

然后在你的身体中,你添加:

 filterstring := array_to_string(filters, ') AND ('))
 IF filterstring = '' or filterstring is null THEN
     filterstring := 'TRUE';
 END IF;

然后,您使用filterstring代替array_to_string来电中的format()来电。

请注意,只要您通过字符串插值在任何地方组装查询,就可以进行SQL注入。

为了防止SQL注入,您需要重新考虑一下您的方法。您最好的选择是尽可能使用format()进行查询。所以:

execute 'SELECT 1 FROM trackings t 
      LEFT JOIN visitors v ON v.id = t.visitor_id
          WHERE v.id = $1'
   USING id;

这导致计划和填充价值发生在两个不同的点上。这在简单参数的情况下效果很好。但是,在动态过滤器的情况下,它不能很好地工作。

不是传入一维数组,而是传递一个二维(nx3数组),每行三个元素。这些是列名,运算符和值。您可以通过传递quote_ident来清理列名称,并通过传递quote_literal来清除值,但是对操作符进行清理可能是个问题所以我的建议是将这些列入白名单并抛出异常,如果找不到运营商。类似的东西:

  DECLARE 
          ...
          op TEXT;
          allowed_ops TEXT[] := ARRAY['=', '<=', '>='];
  BEGIN
       ...
       IF not(op = ANY(allowed_ops)) THEN
           RAISE EXCEPTION 'Illegal operator in function, %', op;
       END IF;
       ...
  END;

这并不容易,但它是可行的。

答案 1 :(得分:2)

由于您开始使用filters in the form of a jsonb array,因此应将其用作函数参数而不是text[]。首先,它将允许您防止SQL注入。

CREATE OR REPLACE FUNCTION match_custom_filter(filters jsonb, id text)
RETURNS boolean LANGUAGE plpgsql AS $$
DECLARE
    f text;
    r boolean;
BEGIN
    IF jsonb_array_length(filters) = 0 THEN
        -- If no filters are specified then run a straight SQL query against trackings
        PERFORM * FROM trackings WHERE visitor_id = quote_literal(id);
        RETURN FOUND;
    ELSE
        -- Build the filters from the jsonb array
        SELECT string_agg(
                   -- Concatenate the parts from a single json object into a filter
                   quote_ident(j->>'field') ||    -- avoid SQL injection on column name
                   CASE j->>'type'
                       WHEN 'greater_than' THEN ' > '
                       ...
                   END ||
                   quote_literal(j->>'value'),    -- avoid SQL injection on value

                   -- Aggregate individual filters with the AND operator
                   ' AND ') INTO f
        FROM jsonb_array_elements(filters) j;

        -- Run a dynamic query with the filters
        EXECUTE format('SELECT true FROM trackings t 
                        LEFT JOIN visitors v ON v.id = t.visitor_id
                        WHERE v.id = %L AND %s LIMIT 1', id, f) INTO r;
        RETURN r;
    END IF;
END $$;

您应该调用此函数传递jsonb数组,如下所示:

SELECT v.*, array_agg(g.name) AS groups
FROM visitors v JOIN groups g ON match_custom_filter(g.group->'filter', v.id)
WHERE v.id = 'cov4pisw00000sjctfyvwq126'
GROUP BY v.id;