アルゴリズムの問題よりも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;
そして、まったく役に立たないIF
とELSE
を追加し、同じRETURN句を使用すると、エラーは発生しません。
私はDBA StackExchangeが開発者向けではないことを知っていますが、この種の問題は、キャッシュの問題などに似ているようです。 PostgreSQLの関数で何か問題が発生している場合、誰かに教えてもらえますか?または、この奇妙な問題についてどこで助けを得ることができますか?
user_info
をrecord
として宣言します。 マニュアル:
レコード変数は行タイプの変数に似ていますが、事前定義された構造はありません。これらは、
SELECT
またはFOR
コマンドの実行中に割り当てられた行の実際の行構造を取ります。 レコード変数のサブ構造は、割り当てられるたびに変更できます。
大胆な強調鉱山。
レコードuser_info
を割り当て、次にrow
ステートメントでレコードから行タイプ(実際にはコードではRETURN
と呼ばれます)を派生させます。同じセッション内の次の呼び出しで、レコードにdifferentデータ型の列を割り当てるまで、これはすべてうまくいきます。これは、準備済みステートメントのキャッシュされたクエリプランと互換性がなく、例外が発生します。
PL/pgSQLは関数本体のすべてのSQLステートメントを準備されたステートメントとして扱います。準備されたステートメントのクエリプランは、関連するオブジェクト(関数自体を含む)が変更されない限り、セッションの期間中キャッシュされます。これにより、依存するすべての準備されたステートメントの割り当てが解除されます。これは、ALTER FUNCTION
の後の次の呼び出しが常に機能する理由を説明しています。詳細な説明:
これにはさまざまな方法があります。あなたはすでにあなた自身を見つけました。異なるデータ型のパラメーターを同じ(準備された)ステートメントにフィードすることは避けてください
簡単な解決策は、同じデータ型にキャストすることです。テーブル定義を提供していません。Iassumeusers.id
およびhistory.id
はinteger
で、完全に一致します。エラーメッセージから判断して、roles.name
はvarchar
であると想定しているため、文字列リテラル'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
も同じ問題の影響を受けることに注意してください。