Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

drizzle-kit migrate は手書きの SQL を適用しない

公開 読了時間 約4分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

drizzle-kit migrate が適用するのは drizzle が生成した DDL だけで、同じリポジトリに手で書いた SQL(RLS ポリシー・監査トリガ・SECURITY DEFINER 関数)は、エラーも警告も出さずに適用されない。

結論

drizzle-kit migrate が適用するのは、drizzle が生成した DDL だけです。

同じリポジトリに手で書いた SQL ── RLS ポリシー、監査トリガ、SECURITY DEFINER 関数 ── を別ディレクトリに置いている場合、それらは適用されません

エラーも警告も出ません。migrate は成功として終わります。

症状

  • マイグレーションは成功する
  • テーブルもカラムも、想定どおりに出来ている
  • アプリは起動する。大半の画面も動く
  • RLS ポリシー・トリガ・関数だけが、本番に存在しない

テーブルが出来ているので、スキーマを目視した限りでは何も欠けていません。欠けているのは「テーブルの形」ではなく「テーブルに掛かっている制御」のほうです。

そして制御は、無くても DDL の適用結果としては矛盾しません。だから誰も止めません。

原因

drizzle のマイグレーションは、drizzle が生成したファイルと、その journal で完結しています

このとき、マイグレーションの実体が2つのディレクトリに分かれていると、こうなります。

drizzle/*.sql      … drizzle が生成した DDL        → drizzle-kit migrate が流す
migrations/*.sql   … 手で書いた RLS / トリガ / 関数 → 流さない

下の段は drizzle から見ればリポジトリに置いてあるだけの無関係なファイルです。読みにも行かないので、「適用しなかった」という報告も出ません。出力に現れないという点が、この問題をしばらく気づかせなくします。

さらに、drizzle 側の journal と生成物がずれると、生成された DDL のほうも意図した通りに流れないことがあります。drizzle-kit migrate 単体を本番適用の手段にできないのは、この2つの理由が重なるためです。

なぜ2つに分かれるのか

drizzle が生成できるのはスキーマ定義から導ける DDL までです。RLS ポリシーの条件式、監査トリガの本体、SECURITY DEFINER 関数の中身は、スキーマ定義には書かれていません。手で書くしかないので、置き場所が分かれます。

**分けたこと自体は間違いではありません。**間違いになるのは、適用する側が片方しか知らないときです。

確認する

スキーマ差分では出ません。カタログを直接引きます。

ポリシー:

SELECT schemaname, tablename, policyname
FROM pg_policies
WHERE schemaname = 'public';

トリガ:

SELECT c.relname AS table_name, t.tgname
FROM pg_trigger t
JOIN pg_class c ON c.oid = t.tgrelid
WHERE NOT t.tgisinternal;

関数:

SELECT proname, prosecdef
FROM pg_proc
WHERE pronamespace = 'public'::regnamespace;

prosecdeftrue なら SECURITY DEFINER です。

**期待している行が0件なら、そのディレクトリは流れていません。**テーブルの有無を見ても分からないので、必ずこちら側を見てください。

直す

適用の単位を「drizzle のコマンド」から「適用順そのもの」に移します。

  1. 適用順を単一のソースに持たせる。 DDL → RLS / トリガ / 関数 の順で、どのファイルをどの順に流すかを1箇所に定義します
  2. 本番でもその順で両方のディレクトリを流す。 drizzle-kit migrate は drizzle 側だけを担当させ、手書き側は同じ定義に従って続けて流します
  3. 適用後にカタログを引くところまでを「適用完了」の定義にする。 上の3本の SELECT を検証手順に入れます

drizzle-kit migrate を呼ばないという話ではありません。それだけでは終わらない、という前提を手順の側に持たせるということです。

適用順の定義を、どこから借りるか

テストのセットアップが既に「両方を正しい順で流す」処理を持っていることがあります。RLS ポリシーやトリガの挙動をテストする以上、テスト環境では両方が入っている必要があるためです。

すでに動いている適用順がそこにあるなら、本番の手順が参照すべき単一のソースはそれです。手順書に順番を書き写すと、片方が増えたときにもう片方が腐ります。

見落としやすい前提

この問題は「テストが弱いから見つからない」のではありません。テストのほうが正しく両方を流しているために、テストだけが完全な状態になり、本番だけが素のテーブルになります。

テストを増やしても検出されません。見るべきなのは、テストと本番で適用の経路が同じかどうかです。

同じ「テストは通るのに本番だけ違う」形の話として、本番だけ SECURITY DEFINER 関数が効かないのは実行ロールのせい も書いています。あちらは同じ SQL を違うロールで流した話で、こちらはその SQL 自体が流れていない話です。どちらも DB は何も文句を言いません。

よくある質問

Q1drizzle-kit migrate は何を適用しますか?

drizzle が生成した DDL、つまり drizzle の出力ディレクトリにある .sql とその journal だけです。同じリポジトリの別ディレクトリに手で書いた SQL は、drizzle から見れば存在しないファイルなので対象外になります。適用されなかったことを知らせる出力も出ません。

Q2適用されたかどうかはどう確認しますか?

スキーマ差分ではなくカタログを直接引きます。ポリシーは pg_policies、トリガは pg_trigger、関数は pg_proc に出ます。テーブルとカラムは drizzle の DDL で作られているため、テーブルの有無を見ても判定できません。

Q3手書きの SQL を drizzle の出力ディレクトリに置けば解決しますか?

その場合は drizzle の journal と手で書いたファイルの整合を人間が維持し続けることになり、生成のたびにずれる余地が残ります。ディレクトリを分けたまま、適用順を単一のソースに持たせて両方を順に流すほうが崩れにくくなります。

Q4テストが通っていれば適用漏れは検出できますか?

テスト側が独自のローダーで両方のディレクトリを流していると、テスト環境にだけポリシーやトリガが存在する状態になります。この場合テストはむしろ全部通り、本番だけが素のテーブルになります。テストの適用経路と本番の適用経路が同じかどうかを先に確認してください。