plpgsql で書いた関数は、参照している列やテーブルが存在しなくても create or replace function が成功します。名前が解決されるのは実行時です。だから migration の適用リストが依存順を見ていないと、ログは全部 OK で終わったのに本番が実行時に落ちます。さらに厄介なことに、pg_proc を覗く検証クエリではこの欠落を検出できませんでした。実際にトランザクション内で関数を呼んで rollback する検証に切り替えた話です。
記事全体の要約。左が「成功」の裏に隠れた実行時エラーの構造、右がカタログ検証と実呼び出し検証の対比です。
自分で運営している高校選びのサービスで、セキュリティ監査の是正をリリースしようとしたときの話です。手順書に書いてあった migration 4 本は、リストに載っていない 3 本に依存していました。片方の関数は family_members.expires_at を、もう片方は admin_pin_attempts を参照していて、どちらもまだ本番に存在しなかった。それでも create or replace function は問題なく通ります。
壊れるのは、誰かがその機能を実際に使った瞬間です。しかも手順書の検証クエリは pg_proc.prosrc の文字列一致を数えるだけだったので、依存が欠けていても期待値どおりの数を返して通ってしまいました。カタログを覗く検証は、名前解決されない plpgsql に対しては無力だったわけです。おまけに migration 自身の自己検証アサーションも、pg_get_function_identity_arguments() の戻り値形式を誤解していて構造的に必ず失敗する書き方になっていました。これは実際に本番へ流して初めて露見しました。
左が手順書に書いてあった 4 本で、全部 OK が並びます。右が実際の依存。左のログだけを見ていると、この食い違いは見えません。
同じ DB に対して、片方は気づけず片方は気づける。実呼び出しでは authentication required で止まることが「そこまでの名前解決は通った」の証拠になります。