金曜16時18分。小さな劇場では、夜公演の開場準備が進んでいた。
予約システムの席種は、seat_typeという列に「一般」「招待」「車いす席」と保存されている。担当エンジニアは、画面を分かりやすくするため、列名をseat_categoryへ変える作業をAIへ頼んだ。
AIはスキーマを直し、マイグレーションとテストを作った。CIはすべて成功。担当者は、AIにここまで任せられると劇場スタッフへ話したばかりだった。予定より遅れていると言いづらく、生成されたSQLを流し読みしてマージした。
16時31分、適用は成功した。ところが受付画面では、1,186件の席種が空欄になっていた。
ALTER TABLE reservations DROP COLUMN seat_type;
ALTER TABLE reservations ADD COLUMN seat_category TEXT;
列名の変更ではなく、古い列を消し、空の新しい列を作っていた。開発DBには作り直せる数件しかなく、テストも新規予約だけを見ていた。
劇場スタッフから「席種が分からないと開場できません」と言われ、すぐ返事ができなかった。
AIが作ったとは言い訳にならない。自分がマージした。分からないと言えば、もう任せてもらえないかもしれない。
そう思うと、コードを戻し、データも一人で直してから報告したくなる。でも、コードのrevertで消えた値まで戻るとは限らない。焦ったまま次のSQLを流せば、残っている復旧材料まで変えるかもしれない😨
スキーマではなく、既存の1行に起きることを見る
スキーマは変更後の形を示す。マイグレーションは、今あるDBをそこへ動かす操作を示す。最終形が同じでも、途中でデータを保つ方法は一つではない。
AIが生成したDB変更では、マージ前に次を分けて確認したい。
- 既存行: 削除する列、必須列の初期値、変換できない値、重複、関連データ
- 実行中: 本番に近い件数での時間、ロック、予約や決済への影響、中止条件
- 戻し方: コード、DB構造、データを何から戻し、何件を照合するか
「テスト成功」は大事な証拠だが、空のDBで新規登録できたことと、既存1,186件を保てることは別の結果だ。
例えば、適用前に件数と値の分布を見る。
SELECT COUNT(*) FROM reservations;
SELECT COUNT(*) FROM reservations WHERE seat_type IS NULL;
SELECT seat_type, COUNT(*)
FROM reservations
GROUP BY seat_type;
ここで知りたいのは、SQLが実行できることではない。「移行対象は1,186件」「空欄は12件」「想定外の席種は2種類」と説明できることだ。
古い列をすぐ消さない
この変更は、1回のリリースで終わらせなくてもよかった。
- 新しい列を空欄可で追加する
- 新旧の列へ同じ値を書く
- 既存値を少しずつ移す
- 件数を照合し、読み取り先を切り替える
- 問題なく動く期間を置き、古い列を削除する
追加、移行、切替、削除へ分ければ、確認できる地点が増える。二重書き込みの監視や期限は必要だが、消してから戻し方を考えるより判断しやすい。
AIへは、完成後のスキーマだけでなく、既存行が各手順でどう変わるか、破壊的な操作、確認SQL、戻せないデータを先に示すよう依頼する。古い列の削除と本番適用は、人の確認で止める。
担当者は、原因を隠して復旧SQLを流す代わりに、事実を短く共有した。
古い列を削除して空の新しい列を作る処理が実行され、1,186件の席種が空欄です。新規書き込みは止めました。バックアップと予約CSVから、復元件数と所要時間を2人で確認します。
同僚はバックアップを復元し、劇場スタッフは紙の控えを準備した。担当者は予約CSVと件数を照合した。一人で失敗を抱えた時より、次に確認する対象が見えた。
テストが通ったことと、今あるデータを失わずに変更できることは別の確認だ。 次のDB変更では、まずマイグレーションを開き、削除される列と、そこに入っている本番データの復元元を確かめたい🙂
