准备好的执行计划上的数据类型错误

时间:2013-09-23 16:49:39

标签: sql postgresql sql-injection plpgsql dynamic-sql

我有以下错误,此错误仅在第二次执行时发生导致计划缓存

ERROR: el tipo del parámetro 11 (character varying) no coincide aquel con que fue preparado el plan (text)
Where: PL/pgSQL function graficar(character varying,character varying,character varying) line 22 at asignación

这意味着下面函数中的变量或值与第22行中的character varying / text不匹配。没有定义明确的文本值,我认为它发生在查询的串联中字符串:

CREATE OR REPLACE FUNCTION graficar(tabla character varying, columna character varying, valor character varying) RETURNS SETOF resultado As $$
    DECLARE
        r resultado;
        i record;
        query character varying = '';
        limite character varying = '';
    BEGIN
    IF columna <> '' THEN
       limite = ' where ' || columna || ' =  $1';
    END IF;

        IF tabla = 'edad' THEN
            query =   'WITH ranges AS ( SELECT (ten*10)::text ||''-''||(ten*10+9)::text AS range, ten*10 AS r_min, ten*10+9 AS r_max FROM generate_series(0,9) AS t(ten)) SELECT r.range as nombre, count(s.*) as cuenta FROM ranges r  LEFT JOIN ( select * from persona '|| limite ||' ) as s ON  s.edad BETWEEN r.r_min AND r.r_max GROUP BY r.range HAVING range NOT IN (''0-9'') ORDER BY r.range;';
        ELSEIF tabla = 'ingreso' THEN
           query =   'WITH ranges AS ( SELECT (ten*10)::text|| ''-''||((ten*10+199))::text || '' mil'' AS range, ten*10/1000 AS r_min, (ten*10+199999)/1000 AS r_max FROM generate_series(0,(SELECT max(ingreso)/10000 FROM persona), 20) AS t(ten)) SELECT r.range as nombre, count(s.*) as cuenta FROM ranges r  LEFT JOIN ( select * from persona '|| limite ||' ) as s ON s.ingreso BETWEEN r.r_min AND r.r_max GROUP BY r.range  ORDER BY r.range;';
        ELSE
            query = 'select p.nombre, count( e.* ) as cuenta from ' || tabla::regclass ||' p left join ( select * from persona ' || limite ||' ) as e on p.nombre = e.'|| tabla::regclass ||' group by p.nombre ';
    END IF;

    FOR i IN EXECUTE query USING valor LOOP  -- I DONT KNOW EXACTLY WHERE LINE 22 I SUPOSSE IS THIS
        r = (i.nombre, i.cuenta);
        RETURN NEXT r;
    END LOOP;
    END
$$ LANGUAGE plpgsql;

2 个答案:

答案 0 :(得分:1)

由于EXECUTE应该在每次运行时重新计划,因此错误不应来自计划缓存问题。

除此之外,这个函数似乎有两个潜在的错误:

1)当columna<>''为false时,动态查询中没有参数,但EXECUTE尝试使用USING valor提交值。

2)当columna<>''为真时,即使该列的类型无法与此值进行隐式比较,它也会将此列与类型character varying的值进行比较。据推测,这需要明确转换为文本:

limite = ' where ' || columna || '::text =  $1';

并让valor参数的类型为text(或保留character varying,但使用CAST语法。

答案 1 :(得分:1)

CREATE OR REPLACE FUNCTION graficar(tabla text, columna text, valor text)
  RETURNS SETOF resultado As
$func$
DECLARE
   _query text;
   limite text := '';
BEGIN
IF columna <> '' THEN
   limite := format(' AND %I = %L', columna, valor); -- properly escaped
END IF;

_query :=
   CASE tabla
   WHEN 'edad' THEN
   $q$WITH ranges AS (
         SELECT concat(ten, '0-', ten, '9') AS range
               ,ten*10 AS r_min, ten*10+9 AS r_max
         FROM   generate_series(1,9) ten)
      SELECT r.range AS nombre, count(p.*)::int AS cuenta
      FROM   ranges       r
      LEFT   JOIN persona p ON p.edad BETWEEN r.r_min AND r.r_max$q$
      || limite || '
      GROUP  BY r.range
      ORDER  BY r.range'

   WHEN 'ingreso' THEN
   $q$WITH ranges AS (
         SELECT concat(ten, '0-', ten*10 + 199, ' mil') AS range
               ,ten*10/1000 AS r_min, (ten*10+199999)/1000 AS r_max
         FROM   generate_series(0,(SELECT max(ingreso)/10000 FROM persona)
                                                             , 20) AS ten)
      SELECT r.range AS nombre, count(p.*)::int AS cuenta
      FROM   ranges r
      LEFT   JOIN persona p ON p.ingreso BETWEEN r.r_min AND r.r_max$q$
      || limite || '
      GROUP  BY r.range
      ORDER  BY r.range'

   ELSE
      format(
   $q$SELECT t.nombre, count(p.*)::int AS cuenta
      FROM   %1$I t
      LEFT   JOIN persona p on p.%1$I = t.nombre$q$ || limite || '
      GROUP  BY t.nombre'
      , tabla)
   END;

RETURN QUERY EXECUTE _query;

END
$func$ LANGUAGE plpgsql;

主要观点:

  • 为简单起见,使用text代替character varying

  • 使用concat()可以更轻松地进行格式化。需要Postgres 9.1 +。

  • 使用人类可读的格式!您发布的字符串很难维护。

  • 为第一种情况生成从1开始的数字,因为无论如何都要排除0的情况。因此,修剪现在多余的HAVING子句。

  • assignment operator in plpgsql is :=不是= - 这通常有效,但是在将来的版本中可能会消失的未记录的功能。

  • 正确使用美元报价。

  • 最后使用简单的RETURN QUERY代替整个LOOP构造。

  • 不要使用query作为变量名,它是plpgsql中的保留字。取而代之的是_query

  • 为了避免潜在的类型不匹配described by @Daniel,请在查询中提供valor作为字符串文字。这是规则的罕见例外!通常,优越的方法是使用USING子句传递值,就像你拥有它一样。但是为了提供一系列可能不同的类型,您最好的选择是提供一个可以自动强制转换为任何类型的无类型字符串文字。这样,表达式仍为sargable,并且可以使用该列可能存在的任何索引。

  • 通过正确转义所有标识符和字符串来避免SQL注入。我主要使用format()。需要Postgres 9.1+。 Details in this related answer on dba.SE.