Rebounder Tech Blog

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

drizzle-kit generateは列追加のsnapshot放置で重複ALTERを再出力する

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

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

結論

drizzleのsnapshotに列追加を手編集で反映していないと、次のdrizzle-kit generateはIF NOT EXISTSの無いADD COLUMNを重複して出力する。

drizzleのsnapshotに列追加を手編集で反映していないと、次のdrizzle-kit generateはIF NOT EXISTSの無いADD COLUMNを重複して出力する。

結論

新テーブルを伴わない列追加では、drizzle-kit generateを再実行せず、snapshot(drizzle/meta/*.json)を直接手編集して列定義を反映させる。 放置したまま次にgenerateを実行すると、drizzle-kitはその列を「まだ存在しない」と判定し、IF NOT EXISTSの無いADD COLUMNを重複して出力する。適用済み環境(手書きmigrationで既にその列が存在する環境)にこのALTERを流すとエラーになる。

症状

2026-07-03(#1205)、新テーブル class_weekly_schedules を追加するために drizzle-kit generate を実行したところ、生成されたmigrationファイルに、既に手書きmigration 0034/0035で追加済みの class_visitors.sort_order / student_callouts.sort_order のADD COLUMNが重複して出力された。

-- (手動除去)drizzle-kit は class_visitors / student_callouts の sort_order ADD COLUMN もここに生成したが、
-- それらは手書き 0034/0035(ADD COLUMN IF NOT EXISTS・適用済み環境あり)が担うため本ファイルからは除去した。
-- スナップショット(meta)には sort_order が取り込まれたので、以後の generate で再出力されるドリフトは解消済み。

0034/0035はADD COLUMN IF NOT EXISTS付きで書かれていたのに対し、drizzle-kitが今回生成したALTERにはIF NOT EXISTSが無い。この回はSQLファイルから重複箇所を手動で除去し、snapshotへsort_orderを取り込んでドリフトを解消する形で収めた。

翌2026-07-04(#1220)、今度はclass_visitors / student_calloutsis_highlight列を追加する対応が発生した。この対応は新テーブルを伴わないため、同じ手順(generate → 重複箇所を手動除去)を踏むと、「is_highlightのADD COLUMNだけを含むmigrationファイル + journalエントリ」が生成され、除去後にはコメントだけの空SQL資材が残る——前回とまったく同じ形の後始末が、今回はメインの変更内容そのものになってしまう。

原因

drizzle-kit generateは、schema定義(TypeScript)と直近のsnapshotを比較して差分だけをSQLとして出力する。snapshot側にその列の記録が無ければ、実際にDBへ存在するかどうかに関わらず「新規追加」として扱われ、対応するALTER TABLE … ADD COLUMNが生成される。このとき生成されるADD COLUMNにIF NOT EXISTSは付与されない。手書きのmigration(0034/0035)は最初からIF NOT EXISTSを付けて書かれていたため適用済み環境でも安全に再実行できたが、drizzle-kit生成側は同じ配慮をしていない。

直す

#1220ではgenerateを再実行する代わりに、対象のsnapshotファイル(drizzle/meta/20260702121657_snapshot.json)を直接手編集し、class_visitorsstudent_calloutsの両テーブル定義にis_highlight列を追加した。

"is_highlight": {
  "name": "is_highlight",
  "type": "boolean",
  "primaryKey": false,
  "notNull": true,
  "default": false
}

列spec は既存のboolean・既定false列と同形にし、両テーブルのsort_order直後に挿入した。手編集がjournal/SQL系列に一切触れていないことは構造上明らかだが、その手編集が「schemaと矛盾しない」ことは目視ではなくdrizzle-kit generateの再実行で確認している。 結果は“No schema changes, nothing to migrate”——schema TSとsnapshotが一致しているとdrizzle自身が判定した合図で、migrationファイルもjournalエントリも生成されなかった。

再発防止

この選択は、前日#1205で一度踏んだ地雷(generateが重複ALTERを出力し、手動除去した結果コメントのみの空SQL資材が残った)を踏まえたものだった。#1220では「同じ地雷をもう一度踏んでから直す」のではなく、**「地雷を踏む手前でsnapshotだけを先に合わせる」**方式に切り替えている。正しさの検証手段も目視ではなく、drizzle-kit generateが返す決定論的な差分ゼロの確認に委ねている点が共通している。

よくある質問

Q1なぜsnapshotを手編集するのですか?generateし直せば良いのでは?

新テーブルを伴わない列追加でgenerateすると、drizzle-kitは既存のsort_order列と同様に対象列を「新規」と判定し、IF NOT EXISTSの無いADD COLUMNを重複出力して適用済み環境でALTERが失敗しうるため。手編集ならjournal/SQL資材を一切生成せずこの経路を回避できる。

Q2手編集が正しいことはどう確認しましたか?

drizzle-kit generateをもう一度実行し、"No schema changes, nothing to migrate" が返ることを確認した。schema TSとsnapshotが一致しているとdrizzle自身が判定した合図で、migrationファイルもjournalエントリも生成されない。

Q3drizzle-kitが生成するADD COLUMNにIF NOT EXISTSが付かないのは仕様ですか?

根拠にあるのは、手書きmigration(0034/0035)がIF NOT EXISTS付きだったのに対し、drizzle-kit generateが同じ列を再生成した際はIF NOT EXISTSが無かったという実測結果のみ。drizzle-kit側の一般仕様までは根拠に無いため断定しない。

確認した環境

  • drizzle-orm ^0.45.2 / drizzle-kit ^0.31.10
  • 2026-07-03(#1205)に重複ALTERを実際に手動除去、2026-07-04(#1220)に同型の対処をsnapshot手編集へ切り替え

この記事の根拠

  • SQLファイル 13〜15行目コミット 77176ae
  • JSONファイル 1857〜1866行目コミット 6641084
  • JSONファイル 2023〜2032行目コミット 6641084

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