14

IF関数を MySQL から PostgreSQLに複製しようとしています。

IF関数の構文は次のとおりです。IF(condition, return_if_true, return_if_false)

次の式を作成しました。

CREATE OR REPLACE FUNCTION if(boolean, anyelement, anyelement)
   RETURNS anyelement AS $$
BEGIN
    CASE WHEN ($1) THEN
    RETURN ($2);
    ELSE
    RETURN ($3);
    END CASE;
    EXCEPTION WHEN division_by_zero THEN
    RETURN ($3);
END;
$$ LANGUAGE plpgsql;

次のようなほとんどのものでうまく機能しif(2>1, 2, 1)ますが、次の場合はエラーが発生します。

if( 5/0 > 0, 5, 0)

ゼロによる致命的なエラー除算。

私のプログラムでは、条件がユーザーによって提供されているため、分母を確認できません。

回避策はありますか?おそらく、最初のパラメーターをブール値から別のものに置き換えることができれば、その場合、関数は例外を発生させて返すように機能します。

4

2 に答える 2

11

PostgreSQLは標準に準拠しています

この動作は、SQL標準で指定されているようです。しかし、それが本当の問題であるケースを見たのはこれが初めてです。通常は、CASE式またはPL/PgSQLBEGIN ... EXCEPTIONブロックを使用して処理します。

MySQLのデフォルトの動作は危険で間違っています。この動作に依存する古いコードをサポートするためにのみ、そのように機能します。strictモードがアクティブな場合(絶対に常にそうする必要があります) 、新しいバージョンで修正されましたが、残念ながらまだデフォルトにはされていません。MySQLを使用する場合は、常にまたはを有効にしてください。STRICT_TRANS_TABLESSTRICT_ALL_TABLES

ANSI標準のゼロ除算は時々苦痛ですが、データ損失を引き起こす間違いからも保護します。

SQLインジェクションの警告、再設計を検討してください

ユーザーから式を実行している場合は、SQLインジェクションの問題が発生している可能性があります。セキュリティ要件によっては、それを受け入れることができる場合もありますが、すべてのユーザーを完全に信頼しないと、かなり悪い結果になります。ユーザーがだまされて、他の場所から悪意のあるコードを入力する可能性があることを忘れないでください。

式ビルダーをユーザーに公開するように再設計し、クエリビルダーを使用してユーザー式からSQLを作成することを検討してください。これははるかに複雑ですが、安全です。

それができない場合は、ユーザーが抽象構文に入力した式を解析し、実行前に検証してから、解析された式に基づいて新しいSQL式を生成できるかどうかを確認してください。そうすれば、少なくとも彼らが書くことができるものを制限することができるので、彼らは表現に厄介なものを滑り込ませません。式を書き直して、ゼロ除算のチェックなどを追加することもできます。代数式のパーサーを見つける(または書く)のは難しいことではありませんが、ユーザーにどのような種類の式を書かせるかによって異なります。

少なくとも、アプリはSELECT、テーブルに対する権限のみを持ち、スーパーユーザーではなく、テーブルを所有していないロール(「ユーザー」)を使用している必要があります。これにより、SQLインジェクションが引き起こす害を最小限に抑えることができます。

ケースは書かれているようにこの問題を解決しません

いずれにせよ、現在、ユーザーからの式を検証および検査できないため、SQL標準CASEステートメントを使用してこれを解決することはできません。if( a/b > 0, a, b)あなたは通常次のようなものを書くでしょう:

CASE
    WHEN b = 0 THEN b
    ELSE CASE 
        WHEN a/b=0 THEN a
        ELSE b
    END
END

これは、分母がゼロの場合を明示的に処理しますが、式を分割できる場合にのみ可能です。

醜い回避策#1

別の解決策は、置換除算演算子または関数を定義することにより、ゼロによる除算の例外を発生させる代わりに、Pgにプレースホルダーを返すようにすることです。これはゼロ除算の場合のみを解決し、他の場合は解決しません。

'NaN'それが論理的な結果なので、私は戻りたかったのです。残念ながら、「NaN」は数値よりも大きく、それ以上であるため、より小さいまたは偽のような結果が必要です。

regress=# SELECT NUMERIC 'NaN' > 0;
 ?column? 
----------
 t
(1 row)

これは、代わりにNULLを返すという厄介なハックを使用する必要があることを意味します。

CREATE OR REPLACE FUNCTION div_null_on_zero(numeric,numeric) returns numeric AS $$
VALUES (CASE WHEN $2 = 0 THEN NULL ELSE $1/$2 END)
$$ LANGUAGE 'SQL' IMMUTABLE;

CREATE OPERATOR @/@ (
    PROCEDURE = div_null_on_zero(numeric,numeric),
    LEFTARG = numeric,
    RIGHTARG = numeric
);

使用法:

regress=# SELECT 5 @/@ 0, 5 @/@ 0>0, CASE WHEN 5 @/@ 0 > 0 THEN 5 ELSE 0 END;
 ?column? | ?column? | case 
----------+----------+------
          |          |    0
(1 row)

アプリは、入力式の「/」を、選択した@/@演算子名または任意の演算子名に簡単に書き換えることができます。

このアプローチには非常に重大な問題が1つあります。それは、明示的な括弧のない式が期待どおりに評価されない可能性があるため、@/@優先順位が異なることです。/これを回避するには、新しいスキーマを作成し、そのスキーマでnull-on-errorトリックを実行する名前付きの演算子を定義してから、ユーザー式を実行する前に/そのスキーマをに追加します。search_pathしかし、それはおそらく悪い考えです。

醜い回避策#2

分母を調べることができないので、私が考えることができるのは、すべてをDOブロック(Pg 9.0+)またはPL / PgSQL関数でラップし、式の評価からの例外をキャッチすることだけです。

アーウィンの答えは私よりも良い例を示しているので、これを削除しました。いずれにせよ、これはひどく危険なことです。やらないでください。アプリを修正する必要があります。

于 2012-09-13T22:06:54.223 に答える
9

ブール値の引数を使用すると、関数が呼び出される前に、ゼロ除算によって常に例外がスローされます (これは良いことです) 。それについてあなたができることは何もありません。それはすでに起こっています。

CREATE OR REPLACE FUNCTION if(boolean, anyelement, anyelement)
 RETURNS anyelement LANGUAGE SQL AS
$func$
SELECT CASE WHEN $1 THEN $2 ELSE $3 END
$func$;

そもそも名前付きの関数を使用しないことを強くお勧めしifます。IFPL/pgSQL のキーワードです。PL/pgSQL で記述されたユーザー定義関数を使用すると、非常に混乱します。

標準の SQL 式CASEを直接使用するだけです。


text別の方法は、引数を取り、それを動的 SQLで評価することです。

コンセプトの証明

あなたが求めるものは次のように機能します:

CREATE OR REPLACE FUNCTION f_if(_expr text
                              , _true anyelement
                              , _else anyelement
                              , OUT result anyelement)
  RETURNS anyelement LANGUAGE plpgsql AS
$func$
BEGIN
   EXECUTE '
   SELECT CASE WHEN (' || _expr || ') THEN $1 ELSE $2 END' -- !! dangerous !!
   USING _true, _else
   INTO result;

   EXCEPTION WHEN division_by_zero THEN
   result := _else;
   -- possibly catch more types of exceptions ...
END
$func$;

テスト:

SELECT f_if('TRUE'   , 1, 2)  --> 1
      ,f_if('FALSE'  , 1, 2)  --> 2
      ,f_if('NULL'   , 1, 2)  --> 2
      ,f_if('1/0 > 0', 1, 2); --> 2

これは、信頼されていないユーザーの手に渡ると、大きなセキュリティ上の危険です。これをより安全にすることに関する@Craigの回答を読んでください。ただし、防弾にする方法がわかりませんし、使用することはありません。

于 2012-09-13T21:19:40.013 に答える