Rebounder Tech Blog

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

マイグレーションファイルの連番が2つ重複していた

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

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

結論

手番号方式のマイグレーションは、片方が衝突を警戒して番号をずらしても、もう一方が同時に別の番号を選んでいる限り衝突を防げない。トランクは自分がpushするまで自分の採番を知らない。

結論

2つのfeatureブランチが、同じ日にそれぞれ独立して 0035 番のマイグレーションファイルを作っていました。 本番DBには両方とも冪等なSQLとして適用済みで、実害はありませんでした。危険なのは実害ではなく、新規のデータベースやstaging環境を連番順に流したときに、どちらが先に実行されるか環境によって変わりうるという潜在的な地雷が残っていたことです。後発の側を 0039 へリネームして解消しました。

症状

supabase/migrations/ の下に、同じ連番を持つファイルが2つ並んでいました。

0034_slot_model.sql
0035_company_apply_fields.sql
0035_must_change_password.sql   ← 重複
0036_relax_term_months_min.sql

0035_company_apply_fields.sql は申込フォームの追加項目(representative_email / billing_email)を companies テーブルに足すだけの内容、0035_must_change_password.sql は初回ログイン時パスワード変更のためのフラグ列を profiles テーブルに足すだけの内容で、どちらも ALTER TABLE ... ADD COLUMN IF NOT EXISTS という冪等なSQLでした。本番DBには両方とも既に適用済みで、稼働には何の影響も出ていませんでした。

原因

2つのコミットの作成時刻を見ると、19時01分と19時42分、同じ日の41分違いでした。

56c5dec  19:01  feat(apply): 申込フォームに...拡充(0035_company_apply_fields.sql)
dd64894  19:42  feat(portal): 初回ログイン時パスワード変更導線を追加(0035_must_change_password.sql)

さらに 0035_company_apply_fields.sql のコメントを読むと、この番号の採番は当てずっぽうではなく、衝突を警戒したうえでの判断だったことがわかります。

--  ⚠️ 番号注意: トランク(feat/zero-touch-automation)の最新は 0034_slot_model。
--    0031(領収証)・0032(役割区分)・0033(効果補正)・0034(枠モデル) は
--    各 feature branch 由来で採番済みのため、衝突回避に 0035 を採番。

この時期、0031 から 0034 まで4件のマイグレーションが別々のfeatureブランチから採番されていて、0035 はそれらを踏まえたうえで選んだ「次の空き番号」でした。衝突を避けようとする意識自体はあり、実際に4回は衝突しませんでした。

それでも5件目で衝突しました。理由は単純で、supabase/migrations/ は連番が手番号(0001 0002 …という単純な整数)で管理されており、「次の番号はいくつか」を判断する材料は、自分のブランチが把握しているローカルなファイル一覧だけだったからです。19時01分のコミットが「トランクの最新は0034」と判断した時点では、19時42分のコミットはまだpushされておらず、どこにも存在していませんでした。もう一方のブランチも同じように独立して「次は0035」と判断していて、どちらも相手の存在を知る手段がありませんでした。

直す

内容自体は変えず、後発の 0035_must_change_password.sql を、当時進行中だった 0037 0038 と衝突しない 0039 へリネームしました。

0035_must_change_password.sql → 0039_must_change_password.sql

ファイルの中身は1バイトも変えていません。両方とも IF NOT EXISTS 付きの追加専用SQLなので、番号を振り直しても再適用は起きず、本番DBの状態には影響しません。連番の意味を「実際に適用された順序」ではなく「今後、新規環境に適用する順序」として立て直す作業でした。

再発防止

この事故が示しているのは、「衝突を警戒する」だけでは並行ブランチでの手番号を守り切れないということです。19時01分のコミットは実際に衝突を警戒し、直前の4件の採番まで把握したうえで番号を選びました。それでも防げなかったのは、判断材料が常に「自分のブランチから見えるローカルな最新状態」でしかなく、同じタイミングで動いている別のブランチの採番は、どちらのpushが先になるかが決まるまで誰にも見えないという構造があったためです。

この記事の根拠には、番号衝突を自動で検出する仕組みを追加したという記述はありません。今回はリネームで解消しましたが、同じ構造が残っている限り、次に2つのブランチが同じタイミングで採番すれば同じことが起こり得ます。

よくある質問

Q1同じ連番のマイグレーションファイルが2つあると何が起きますか?

ファイル名の辞書順で適用順が決まる仕組みでは、同じ番号のファイルが2つあること自体はエラーになりません。両方とも実行はされます。危険なのは、環境によって2つのうちどちらが先に流れるかが保証されなくなることです。片方がもう片方の存在を前提にしたSQLだった場合、順序が逆になった環境だけ失敗します。

Q2本番で実害が無かったのはなぜですか?

2つのマイグレーションが互いに独立したテーブル・カラムを追加するだけの冪等なSQL(IF NOT EXISTS付き)だったためです。順序に依存する処理が無かったので、どちらが先でも結果は同じになりました。内容が独立していなければ、適用順序が変わる環境で失敗していました。

Q3採番の衝突は今回が初めてだったのですか?

いいえ。同じ時期に4つのfeatureブランチが並行していて、後発の側は先行する0031〜0034を把握したうえで0035を選んでいました。衝突を警戒していたにもかかわらず、同じタイミングでもう一つ別のブランチが同じ0035を選んでいたため、5件目で初めて衝突しました。

Q4タイムスタンプ採番にすれば防げますか?

衝突の起きにくさは上がりますが、ブランチをまたいだ「次の番号」を誰も一元管理していないという構造は残ります。この記事の事故は番号の桁数や採番方式そのものではなく、並行するブランチが互いの採番をリアルタイムに見えない状態で作業していたことが根本です。

確認した環境

  • Supabase(連番プレフィックスのマイグレーション)
  • 2026-06-14 のコミット時点

この記事の根拠

  • SQLファイル 1〜4行目コミット e74b9bf
  • SQLファイル 1〜14行目コミット 56c5dec

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。