web-dev-qa-db-ja.com

関数が2回使用されたときのPL / pgSQLの問題(キャッシュの問題?)

アルゴリズムの問​​題よりもPostgresのバグのように感じられる、まったく奇妙な問題に直面しています。

私はこの機能を持っています:

CREATE FUNCTION sp_connect(mail character varying, passwd character varying, role character varying)
  RETURNS json LANGUAGE plpgsql STABLE AS
$$
DECLARE
    user_info record;
BEGIN
  IF role = 'Role1' THEN
    SELECT u.id, r.name INTO user_info
    FROM users u
    INNER JOIN users_roles ur ON ur.user_id = u.id
    INNER JOIN roles r ON ur.role_id = r.id
    WHERE u.email = mail
      AND u.password = encode(digest(CONCAT(passwd, u.password_salt), 'sha512'), 'hex')
      AND r.name = 'Role1';
  ELSIF role = 'Role2' THEN
    SELECT h.id, 'Role1' AS name INTO user_info
    FROM history h
    WHERE h.email = mail
      AND h.password = encode(digest(CONCAT(passwd, h.password_salt), 'sha512'), 'hex');
  ELSE
    RAISE 'USER_NOT_FOUND';
  END IF;

  IF NOT FOUND THEN
      RAISE 'USER_NOT_FOUND';
  ELSE
      RETURN row_to_json(row) FROM (SELECT user_info.id AS id, user_info.name AS role) row;
  END IF;
END;
$$;

私が直面している問題は、この関数を使用してRole1-userでログインした後、Role2-userで使用すると、次のエラーメッセージが表示されることです。

type of parameter 7 (character varying) does not match that when preparing the plan (unknown)

それは……まあ、それがどこから来たのかわからない。データベースをワイプしてログインの順序(Role2からRole1)を変更すると、今回はRole1でエラーが発生します。

奇妙な問題、奇妙な解決策... ALTER FUNCTION sp_connectしかし、関数内で何も変更しなくても、魔法のように、2つのロールは問題なくログインできます。私もこの解決策を試しました:

  IF NOT FOUND THEN
      RAISE 'USER_NOT_FOUND';
  ELSE
      IF role = 'Seeker'
      THEN
          RETURN row_to_json(row) FROM (SELECT user_info.id AS id, user_info.name AS role) row;
      ELSE
          RETURN row_to_json(row) FROM (SELECT user_info.id AS id, user_info.name AS role) row;
  END IF;

そして、まったく役に立たないIFELSEを追加し、同じRETURN句を使用すると、エラーは発生しません。

私はDBA StackExchangeが開発者向けではないことを知っていますが、この種の問題は、キャッシュの問題などに似ているようです。 PostgreSQLの関数で何か問題が発生している場合、誰かに教えてもらえますか?または、この奇妙な問題についてどこで助けを得ることができますか?

6
DeadEye

説明:

user_inforecordとして宣言します。 マニュアル:

レコード変数は行タイプの変数に似ていますが、事前定義された構造はありません。これらは、SELECTまたはFORコマンドの実行中に割り当てられた行の実際の行構造を取ります。 レコード変数のサブ構造は、割り当てられるたびに変更できます

大胆な強調鉱山。

レコードuser_infoを割り当て、次にrowステートメントでレコードから行タイプ(実際にはコードではRETURNと呼ばれます)を派生させます。同じセッション内の次の呼び出しで、レコードにdifferentデータ型の列を割り当てるまで、これはすべてうまくいきます。これは、準備済みステートメントのキャッシュされたクエリプランと互換性がなく、例外が発生します。

PL/pgSQLは関数本体のすべてのSQLステートメントを準備されたステートメントとして扱います。準備されたステートメントのクエリプランは、関連するオブジェクト(関数自体を含む)が変更されない限り、セッションの期間中キャッシュされます。これにより、依存するすべての準備されたステートメントの割り当てが解除されます。これは、ALTER FUNCTIONの後の次の呼び出しが常に機能する理由を説明しています。詳細な説明:

解決

これにはさまざまな方法があります。あなたはすでにあなた自身を見つけました。異なるデータ型のパラメーターを同じ(準備された)ステートメントにフィードすることは避けてください

簡単な解決策は、同じデータ型にキャストすることです。テーブル定義を提供していません。Iassumeusers.idおよびhistory.idintegerで、完全に一致します。エラーメッセージから判断して、roles.namevarcharであると想定しているため、文字列リテラル'Role1'varcharにキャストすると、すべてが機能するはずです。 (実際には'Role2'を意味するかもしれませんが、それは問題とは直交しています。)

型付けされていない文字列リテラルは、一部のタイプのSQLステートメントで一致するデータ型に強制変換されます。この場合、データ型はコンテキストから派生できます。しかし、ここではそうではありません。明示的なキャストを行わない場合、文字列リテラルはunknown型であり、これはvarchar(またはtext)とnotと同じではありません。これはエラーメッセージにも表示されます。

CREATE FUNCTION sp_connect(mail varchar, passwd varchar, role varchar)
  RETURNS json AS
$func$
DECLARE
   user_info record;
BEGIN
  IF role = 'Role1' THEN
    SELECT u.id, r.name INTO user_info
    FROM   users u
    JOIN   users_roles ur ON ur.user_id = u.id
    JOIN   roles r ON ur.role_id = r.id
    WHERE  u.email = mail
    AND    u.password = encode(digest(CONCAT(passwd, u.password_salt), 'sha512'), 'hex')
    AND    r.name = 'Role1';

    RAISE NOTICE 'Role1: user_info.big_id: %; user_info.name: %'
                , pg_typeof(user_info.big_id), pg_typeof(user_info.name);  -- see data types
  ELSIF role = 'Role2' THEN
    SELECT h.id, 'Role1'::varchar AS name INTO user_info  -- Cast! And did you mean 'Role2'?
    FROM   history h
    WHERE  h.email = mail
    AND    h.password = encode(digest(CONCAT(passwd, h.password_salt), 'sha512'), 'hex');

    RAISE NOTICE 'Role2: %: user_info.big_id: %; user_info.name: %'
                , pg_typeof(user_info.big_id), pg_typeof(user_info.name);  -- see data types
  END IF;

  IF NOT FOUND THEN
      RAISE 'USER_NOT_FOUND';
  ELSE
      RETURN row_to_json(row) FROM (SELECT user_info.id AS id, user_info.name AS role) row;
  END IF;
END
$func$  LANGUAGE plpgsql STABLE;

関数はSTABLEにすることができます。これは正しい変動率です。

また、RAISE NOTICEステートメントを追加して、デバッグに役立つデータ型を表示しました。 RAISEステートメント自体のプランもキャッシュされるため、END IFの後の単一のRAISEも同じ問題の影響を受けることに注意してください。

10