web-dev-qa-db-ja.com

PostgreSQL関数パラメーターとしてのテーブル名

Postgres関数のパラメーターとしてテーブル名を渡したいです。私はこのコードを試しました:

_CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
    BEGIN
    IF EXISTS (select * from quote_ident($1) where quote_ident($1).id=1) THEN
     return 1;
    END IF;
    return 0;
    END;
$$ LANGUAGE plpgsql;

select some_f('table_name');
_

そして、私はこれを得ました:

_ERROR:  syntax error at or near "."
LINE 4: ...elect * from quote_ident($1) where quote_ident($1).id=1)...
                                                             ^

********** Error **********

ERROR: syntax error at or near "."
_

そして、これはこのselect * from quote_ident($1) tab where tab.id=1に変更されたときに私が得たエラーです:

_ERROR:  column tab.id does not exist
LINE 1: ...T EXISTS (select * from quote_ident($1) tab where tab.id...
_

おそらく、quote_ident($1)が機能するのは、where quote_ident($1).id=1部分がなければ_1_が得られるためです。これは何かが選択されていることを意味します。最初のquote_ident($1)が動作し、2番目の_が同時に動作しないのはなぜですか?そして、これはどのように解決できますか?

66
John Doe

これはさらに簡素化および改善できます。

_CREATE OR REPLACE FUNCTION some_f(_tbl regclass, OUT result integer) AS
$func$
BEGIN
   EXECUTE format('SELECT (EXISTS (SELECT FROM %s WHERE id = 1))::int', _tbl)
   INTO result;
END
$func$  LANGUAGE plpgsql;
_

スキーマ修飾名で呼び出します(以下を参照):

_SELECT some_f('myschema.mytable');  -- would fail with quote_ident()
_

または:

_SELECT some_f('"my very uncommon table name"');
_

主なポイント

  • OUT parameterを使用して、関数を単純化します。動的SQLの結果を直接選択して実行できます。追加の変数とコードは必要ありません。

  • EXISTSはまさにあなたが望むことをします。行が存在する場合はtrue、そうでない場合はfalseを取得します。これにはさまざまな方法がありますが、EXISTSは通常最も効率的です。

  • あなたはintegerを戻したいので、booleanからEXISTSの結果をintegerにキャストします。まさにあなたが持っていたものをもたらします。代わりに boolean を返します。

  • オブジェクト識別子タイプ regclass を__tbl_の入力タイプとして使用します。それはすべてを行います quote_ident(_tbl) または format('%I', _tbl) が行いますが、より良い理由は:

    • ..SQLインジェクションも防止します。

    • ..テーブル名が無効である/存在しない/現在のユーザーから見えない場合、すぐに失敗します。 (regclassパラメーターは、existingテーブルにのみ適用可能です。)

    • ..あいまいさを解決できないため、プレーンなquote_ident(_tbl)またはformat(%I)が失敗するスキーマ修飾テーブル名で動作します。スキーマ名とテーブル名を別々に渡してエスケープする必要があります。

  • 私はまだ format() を使用します。これは、構文を単純化するため(および使用方法を示すため)、ただし、_%s_ではなく_%I_を使用します。通常、クエリはより複雑であるため、format()がより役立ちます。単純な例では、連結することもできます。

    _EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
    _
  • idリストにはテーブルが1つしかありませんが、FROM列をテーブル修飾する必要はありません。この例ではあいまいさはありません。 (動的)EXECUTE内のSQLコマンドには個別のスコープがあり、関数変数またはパラメーターはそこに表示されません-関数本体。

PostgreSQL 9.1でテスト済み。 format()には、少なくともそのバージョンが必要です。

動的SQLのユーザー入力を適切にalwaysエスケープする理由は次のとおりです。

db <> fiddle hereSQLインジェクションを示しています。
古い sqlfiddle

100

これをしないでください。

それが答えです。それはひどいアンチパターンです。どんな目的に役立ちますか?クライアントがデータを必要としているテーブルを知っている場合、SELECT FROM ThatTable!これが必要な方法でデータベースを設計した場合、おそらくそれは間違って設計しているでしょう。データアクセスレイヤーが値がテーブルに存在するかどうかを知る必要がある場合、そのコードで動的SQLの部分を実行するのは簡単です。データベースにプッシュするのは良くありません。

アイデアがあります:エレベーター内にデバイスを設置して、希望の階数を入力してみましょう。次に、「Go」を押すと、目的のフロアの正しいボタンにメカニカルハンドが移動し、ボタンが押されます。革命的!

どうやら私の答えは説明が短すぎたので、この欠陥をより詳細に修復しています。

私はm笑するつもりはありませんでした。私の愚かなエレベーターの例は私が想像できる最高のデバイス質問で提案されたテクニックの欠陥を簡潔に指摘したことです。この手法は、完全に役に立たないインダイレクションレイヤーを追加し、堅牢で十分に理解されているDSL(SQL)を使用する呼び出し元スペースから、あいまい/奇妙なサーバー側SQLコードを使用するハイブリッドにテーブル名の選択を不必要に移動します。

クエリ構築ロジックを動的SQLに移行することにより、このような責任が分割されると、コードが理解しにくくなります。エラーが発生する可能性のあるカスタムコードの名前の完全に合理的な規則(SQLクエリが選択対象を選択する方法)を破壊します。

  • ダイナミックSQLは、フロントエンドコードまたはバックエンドコードで単独で認識するのが難しいSQLインジェクションの可能性を提供します(これを確認するには一緒に検査する必要があります)。

  • ストアドプロシージャと関数は、SP /関数の所有者には権限があり、呼び出し元にはないリソースにアクセスできます。私の知る限り、動的SQLを生成して実行するコードを使用すると、データベースは呼び出し元の権限で動的SQLを実行します。これは、特権オブジェクトをまったく使用できないか、すべてのクライアントに公開する必要があるため、特権データに対する潜在的な攻撃の対象領域が増えることを意味します。作成時にSP /関数を常に特定のユーザーとして実行するように設定(SQL Serverでは、EXECUTE AS)はその問題を解決するかもしれませんが、事態をより複雑にします。これは、動的SQLを非常に魅力的な攻撃ベクトルにすることで、前のポイントで述べたSQLインジェクションのリスクを悪化させます。

  • 開発者がアプリケーションコードを修正したりバグを修正したりするためにアプリケーションコードが何をしているのかを理解する必要がある場合、正確なSQLクエリを実行するのは非常に困難です。 SQLプロファイラーを使用できますが、これには特別な特権が必要であり、実動システムにパフォーマンス上の悪影響を与える可能性があります。実行されたクエリは、SPで記録できますが、これは理由なしに複雑さを増し(新しいテーブルの維持、古いデータの削除など)、完全に非自明です。実際、一部のアプリケーションは開発者がデータベース資格情報を持たないように設計されているため、送信されているクエリを実際に見ることはほとんど不可能になります。

  • 存在しないテーブルを選択しようとした場合など、エラーが発生すると、データベースから「無効なオブジェクト名」の行に沿ってメッセージが表示されます。バックエンドでSQLを作成する場合でも、データベースでSQLを作成する場合でも、まったく同じように発生しますが、違いは、システムのトラブルシューティングをしようとしている貧しい開発者の中には、問題が実際に存在するので、Does It Allの不思議な手順を掘り下げて、問題が何であるかを理解してください。ログには「GetWidgetのエラー」は表示されず、「OneProcedureToRuleThemAllRunnerのエラー」が表示されます。この抽象化は、システムを単にworseにします。

以下は、パラメーターに基づいてテーブル名を切り替える擬似C#のはるかに良い例です。

string sql = string.Format("SELECT * FROM {0};", EscapeSqlIdentifier(tableName));
results = connection.Execute(sql);

他の手法で述べたすべての欠陥は、この例にはまったくありません。

ストアドプロシージャにテーブル名を送信することには、目的も利益もありません。

14
ErikE

Plpgsqlコードの内部では、テーブル名または列が変数に由来するクエリには [〜#〜] execute [〜#〜] ステートメントを使用する必要があります。また、queryが動的に生成される場合、IF EXISTS (<query>)構造は許可されません。

以下は、両方の問題が修正された関数です。

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
DECLARE
 v int;
BEGIN
      EXECUTE 'select 1 FROM ' || quote_ident(param) || ' WHERE '
            || quote_ident(param) || '.id = 1' INTO v;
      IF v THEN return 1; ELSE return 0; END IF;
END;
$$ LANGUAGE plpgsql;
9
Daniel Vérité

最初のものは、あなたが意味する意味で実際に「機能する」のではなく、エラーを生成しない範囲でのみ機能します。

SELECT * FROM quote_ident('table_that_does_not_exist');を試してください。関数が1を返す理由がわかります。selectは、1つの列(quote_ident)1行(変数$1またはこの特定の場合table_that_does_not_exist)。

あなたがしたいことは動的SQLを必要とします。それは実際にはquote_*関数は使用するためのものです。

3
Matt

質問がテーブルが空かどうか(id = 1)をテストすることであった場合、Erwinのストアドプロシージャの簡易バージョンは次のとおりです。

CREATE OR REPLACE FUNCTION isEmpty(tableName text, OUT zeroIfEmpty integer) AS
$func$
BEGIN
EXECUTE format('SELECT COALESCE ((SELECT 1 FROM %s LIMIT 1),0)', tableName)
INTO zeroIfEmpty;
END
$func$ LANGUAGE plpgsql;
1
Julien Feniou

テーブル名、列名、値をパラメーターとして関数に動的に渡す場合

このコードを使用

create or replace function total_rows(tbl_name text, column_name text, value int)
returns integer as $total$
declare
total integer;
begin
    EXECUTE format('select count(*) from %s WHERE %s = %s', tbl_name, column_name, value) INTO total;
    return total;
end;
$total$ language plpgsql;


postgres=# select total_rows('tbl_name','column_name',2); --2 is the value
0
Sandip Debnath