導入
eurekapu-nuxt4のDB周りを触るたびに、既存のクエリビルダの挙動で手が止まる場面が増えていた。D1マイグレーションを3本流した翌日、「このままKyselyベースで進めて大丈夫なのか」という疑問がターミナルの向こうに居座り続けた。今日はその疑問を正面から潰しにいった。
4並行エージェントでプロジェクト構造を深掘り
まずeurekapu-nuxt4の全体像を正確に掴むため、Claude Codeの並行エージェント機能を使って4つの調査を同時に走らせた。
- エージェント1: ディレクトリ構成とNuxt設定の解析
- エージェント2: DBスキーマとマイグレーション履歴の読み込み
- エージェント3: 認証フロー(Better Auth)の全パス追跡
- エージェント4: サーバーAPI・ミドルウェアの依存関係マッピング
4つの結果をresearch.mdに統合した。Boris Taneのワークフローで言う「リサーチフェーズ」をそのまま実行した形になる。リサーチが間違っていればその先の計画も全部間違う -- という原則は前日に翻訳した記事の通りだった。
@atinux/kysely-d1の正体
調査で分かったのは、プロジェクトが@atinux/kysely-d1というパッケージに依存していること。Nuxt作者のAtinux(Sebastien Chopin)がCloudflare D1向けにKyselyアダプタを薄くラップしたもので、中身を見ると以下のような構成だった。
- Kysely本体のDialectインターフェースを実装
- D1のHTTP APIをドライバとして接続
- 型安全なクエリビルダを提供するが、マイグレーション管理は別途必要
問題は、Kyselyの型定義を手動で維持する必要がある点。テーブルを追加・変更するたびにインターフェース定義を手書きで更新していた。スキーマとの乖離が静かに積もっていく構造だった。
なぜDrizzle ORMか
Drizzle ORMを選んだ理由は3つ。
- スキーマ定義がシングルソース: TypeScriptでテーブルを定義すると、型もマイグレーションもそこから生成される。手動同期が消える
- D1ネイティブサポート:
drizzle-orm/d1が公式で提供されており、@atinuxの薄いラッパーに頼る必要がない - drizzle-kit:
drizzle-kit generateでマイグレーションSQLを自動生成し、drizzle-kit pushでD1に直接適用できる。前日のマイグレーション手書き地獄から解放される
// Drizzleのスキーマ定義例(イメージ)
export const users = sqliteTable('users', {
id: text('id').primaryKey(),
email: text('email').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }),
})
この定義から型推論もマイグレーションも自動で得られる。
移行計画の策定
research.mdをベースに、Drizzle ORM移行計画を策定した。計画の骨子は以下の通り。
フェーズ1: スキーマ定義の移植
既存のD1テーブル(Better Authのテーブル群 + アプリ固有テーブル)をDrizzleのスキーマ定義に書き起こす。既存データは維持したまま、クエリビルダだけ差し替える。
フェーズ2: クエリの段階的移行
Kyselyで書かれたクエリを1ファイルずつDrizzleに置き換える。テストが通ることを毎回確認しながら進める。
フェーズ3: @atinux/kysely-d1の除去
全クエリの移行が完了したら、Kyselyへの依存を完全に削除する。
フェーズ4: drizzle-kitによるマイグレーション管理へ移行
手書きSQLマイグレーションからdrizzle-kit generateベースの運用に切り替える。
Codexレビュー: テーブル名混同リスクの指摘
計画をmemo/に保存した後、Codex(gpt-5.3-codex)にレビューを依頼した。
返ってきた指摘の中で刺さったのが、「物理テーブル名とTypeScript変数名の混同リスク」だった。
Drizzleでは以下のように書く。
// 物理テーブル名: 'quiz_answer' (snake_case)
// TypeScript変数名: quizAnswer (camelCase)
export const quizAnswer = sqliteTable('quiz_answer', { ... })
Better Authが生成するテーブル(user, session, account, verification)は物理名がsingularで、アプリ側のテーブル(quiz_answers, access_info)はpluralだったりする。この混在が、Drizzleのスキーマ定義時に「変数名にどちらの命名規則を使うか」で混乱を招く。
Codexの指摘を受けて、計画に以下のルールを追記した。
- 物理テーブル名: 既存のD1テーブル名をそのまま維持(データ移行を避ける)
- TypeScript変数名: 物理テーブル名をcamelCaseに変換したものを使う(
quiz_answer->quizAnswer) - 命名規則の対応表: スキーマファイルの冒頭にコメントで明記する
些末な指摘ではなく、実装フェーズで確実にハマるポイントだった。計画段階で潰せたのはCodexレビューの効果そのもの。
成果物
research.md: eurekapu-nuxt4の全体構造レポート(4エージェント調査の統合結果)memo/2026-02-23/drizzle-orm-migration-plan.md: Drizzle ORM移行計画(Codexレビュー反映済み)
学びメモ
リサーチ→計画→レビューのパイプラインが回った
前日に翻訳したBoris Taneのワークフローを初めて意識的に実践した。4並行エージェントでresearch.mdを作り、そこから計画を書き、Codexにレビューさせる。手を動かす前に計画の穴をレビューで塞ぐという流れが、実際にテーブル名混同リスクを捕まえた。「計画をレビューに出すと、実装で踏む地雷が先に見つかる」という感触を掴んだ。
薄いラッパーの寿命
@atinux/kysely-d1のようなエコシステムの隙間を埋めるパッケージは、公式サポートが追いつくと役目を終える。Drizzle ORMのD1サポートが成熟した今、薄いラッパーを維持する理由が消えた。依存パッケージを選ぶときに「公式が追いつくまでの橋渡しか、長期的に使うものか」を区別する視点が要る。
Codexレビューの使いどころ
「瑣末な指摘をするな、致命的な点だけ指摘しろ」という指示を入れたことで、ノイズが減って実質的な指摘が返ってきた。テーブル名混同リスクは、自分一人では「書けば分かるだろう」と流していた可能性が高い。第三者の目が計画段階で入る価値を体感した。
振り返り
D1マイグレーションを手書きで3本流した翌日に「このやり方は続かない」と判断して、Drizzle移行の計画を一日で書き上げた。手を動かす前にresearch.mdと計画を揃え、Codexレビューで穴を塞いでから保存するという流れを、今回初めて一連のパイプラインとして完走した。明日からはこの計画に沿って、実際にスキーマ定義の移植から手を付けていく。