如何在 Postgres 9.1+ 中实现可变参数 ICase

How to implement variable parameter ICase in Postgres 9.1+

以下代码用于在 Postgres 9.1 及更高版本中实现 ICase() (Switch in VB) 函数。

它有一些限制:

  1. 参数个数固定为3,实际上icase的参数个数可以从4到2*n+1

  2. resi 参数具有固定类型 text。 ICase 还应接受 resi 的数字、小数、日期或其他数据类型,并且 return 值应与 resi

  3. 的类型相同

如何解决这个问题? 还是为每个可能的参数数量和 rosi 类型创建单独的重载更好?

CREATE OR REPLACE FUNCTION public.ICase(cond1 bool, res1 text, cond2 bool, res2 text,cond3 bool, res3 text, conddefault text )
  RETURNS TEXT AS
$BODY$
SELECT CASE when  then  
            when  then 
            when  then 
else  end;
$BODY$ language sql immutable;

更新

我按照答案试了

CREATE OR REPLACE FUNCTION public.icase(
    cond1 boolean,
    res1 anyelement,
    conddefault anyelement)
  RETURNS anyelement AS
' SELECT CASE WHEN  THEN  ELSE  END; '
  LANGUAGE sql IMMUTABLE;

声明

select icase( true, 1.0, 0 )

导致错误

ERROR:  function icase(boolean, numeric, integer) does not exist
LINE 9: select icase( true, 1.0, 0 )
               ^
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.

如何在 9.1+ 中解决此问题,以便第二个和第三个参数可以是整数和数字?

请参阅下面的编辑以了解新方法。

如果可以将您的输入作为数组提供,您可以使用:

CREATE FUNCTION public.ICase(
  p_cond boolean[],
  p_array anyarray)
RETURNS anyelement AS
$BODY$
DECLARE
  v_arrlen integer;
BEGIN
  v_arrlen := array_upper(p_array, 1);
  IF v_arrlen <> array_upper(p_cond, 1) + 1 THEN
    RETURN NULL;
  END IF;
  FOR i IN 1..v_arrlen LOOP
    IF p_cond[i] THEN
      RETURN p_array[i];
    END IF;
  END LOOP;
  RETURN p_array[v_arrlen];
END;
$BODY$
LANGUAGE plpgsql IMMUTABLE;

尝试使用以下方式呼叫:

SELECT * FROM ICase(ARRAY[FALSE, FALSE, TRUE], ARRAY[1, 2, 3, 4])

产生结果 3

编辑:好吧,既然你真的想保持你的原始调用完好无损,我认为如果你用动态查询创建这个函数的所有可能排列可能是最简单的。它很丑,但它有效。

CREATE OR REPLACE FUNCTION build_icase(p_num integer, p_types text[]) RETURNS VOID AS 
$BODY$ 
DECLARE
  v_qry text;
BEGIN 
FOR i IN 1..p_num LOOP
  FOR j IN 1..array_upper(p_types, 1) LOOP
    v_qry := 'CREATE OR REPLACE FUNCTION icase(';
    FOR k IN 1..i LOOP
        IF k > 1 THEN v_qry := v_qry || ', '; END IF;
        v_qry := v_qry || 'cond' || k || ' boolean, res' || k || ' ' || p_types[j];
    END LOOP;
    v_qry := v_qry || ', conddefault ' || p_types[j] || ') RETURNS ' || p_types[j] || ' AS $FUNC$ SELECT CASE ';
    FOR k in 1..(i * 2) BY 2 LOOP
        v_qry := v_qry || 'WHEN $' || k || ' THEN $' || k + 1 || ' ';
    END LOOP;
    v_qry := v_qry || 'ELSE $' || (i * 2) + 1 || ' END; $FUNC$ LANGUAGE sql IMMUTABLE;';
    EXECUTE v_qry;
  END LOOP;
END LOOP;
END; 
$BODY$ LANGUAGE plpgsql VOLATILE;

然后为 max-conditionals/results of 10 和结果数据类型 'date'、'numeric' 或 'text, 运行:[= 构建批量函数21=]

SELECT build_icase(10, ARRAY['date', 'numeric', 'text'])

这将为您需要传递多少条件参数的所有迭代构建 30 个函数,以及结果的所有可能数据类型。做完这个之后,我意识到我可能已经用 anyelement 抽象了它,这将把它减少到 10 个函数(见下面的 EDIT2)。我在 anyelement 方面没有太多经验,所以我不确定它 运行 是否会比使用本例中的显式数据类型时出现更多的转换错误。

在 运行 执行此操作时要小心,因为它可能会创建很多功能供您潜在地清理。我建议使用较少的数字进行测试,您始终可以将 EXECUTE v_qry; 替换为 RAISE INFO 'v_qry is: %', v_qry;,这只会将动态查询作为消息打印出来而不执行它们。

EDIT2:下面是一个使用anyelement创建较少功能的版本。我没有测试这个,但从这里看起来不错。

CREATE OR REPLACE FUNCTION build_icase(p_num integer) RETURNS VOID AS 
$BODY$ 
DECLARE
  v_qry text;
BEGIN 
FOR i IN 1..p_num LOOP
    v_qry := 'CREATE OR REPLACE FUNCTION icase(';
    FOR k IN 1..i LOOP
        IF k > 1 THEN v_qry := v_qry || ', '; END IF;
        v_qry := v_qry || 'cond' || k || ' boolean, res' || k || ' anyelement';
    END LOOP;
    v_qry := v_qry || ', conddefault anyelement) RETURNS anyelement AS $FUNC$ SELECT CASE ';
    FOR k in 1..(i * 2) BY 2 LOOP
        v_qry := v_qry || 'WHEN $' || k || ' THEN $' || k + 1 || ' ';
    END LOOP;
    v_qry := v_qry || 'ELSE $' || (i * 2) + 1 || ' END; $FUNC$ LANGUAGE sql IMMUTABLE;';
    EXECUTE v_qry;
END LOOP;
END; 
$BODY$ LANGUAGE plpgsql VOLATILE;

从 PostgreSQL 9.4 开始,目前无法创建参数行为与 VB 中的 ICase() 参数行为相同的 PostgreSQL 函数。

当前的两个限制是:

  1. VARIADIC 只能在函数末尾指定一次(因此 2*n+1 本身不可执行)。
  2. 虽然支持 polymorphic arguments,但每次调用只能指定一种数据类型,因为它将 VARIADIC 参数转换为 ARRAY(因此 ARRAY[TRUE, 'text'] 无效) .

但是,如果我们重新排列参数,使用数组和 pseudo-type 个参数可以实现非常相似的行为:

CREATE OR REPLACE FUNCTION public.ICase(boolean[], anyarray, anyelement)
  RETURNS anyelement AS
$BODY$
SELECT
  [i]
FROM
  generate_subscripts(, 1) g(i)
WHERE
  [i] IS TRUE
UNION ALL
SELECT
  
LIMIT 1;
$BODY$ LANGUAGE sql IMMUTABLE;
  1. 第一个参数接受条件 boolean 值的数组。
  2. 第二个参数接受任何数据类型的值数组。
  3. 第三个参数接受任何数据类型,但它必须与第二个参数数组元素的数据类型相匹配。

</code> 的 <code>boolean[] 的第一个 TRUE 元素将导致返回 </code> 的相同索引:</p> <pre><code>SELECT ICase(ARRAY[0=1,1=1], ARRAY['a','b'], 'default'); icase ------- b (1 row)

如果</code>内没有<code>TRUE个元素,则返回</code>:</p> <pre><code>SELECT ICase(ARRAY[FALSE,FALSE], ARRAY['a','b'], 'default'); icase --------- default (1 row)

</code> 和 <code> 支持任何数据类型:

SELECT ICase(ARRAY[TRUE], ARRAY['12:00'::time], NOW()::time);
  icase   
----------
 12:00:00
(1 row)

SELECT ICase(ARRAY[FALSE], ARRAY[1], -1);
 icase 
-------
    -1
(1 row)

理论上您可以创建一个由 boolean 和另一种数据类型组成的复合类型,并创建一个类似 ICaseText(text, VARIADIC boolean_text[]) 的函数,但它不会像上面的示例那样灵活。

我不确定 VB 的 ICase(),但是使用这样的函数不允许您使用像内置 [=41= 这样的子表达式的短路] 确实如此,例如:

SELECT CASE WHEN TRUE THEN 1 ELSE 1/0 END;
 case 
------
    1
(1 row)

1/0 从未实际计算过,所以我们不会得到 "division by zero" 错误。

在使用ICase函数时,同样不成立:

SELECT ICase(ARRAY[TRUE], ARRAY[1], 1/0);
ERROR:  division by zero