• #eurekapu
  • #nuxt4
  • #drizzle-orm
  • #cloudflare-d1
  • #database
  • #migration
  • #claude-code
開発eurekapu-nuxt4メモ

導入

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つ。

  1. スキーマ定義がシングルソース: TypeScriptでテーブルを定義すると、型もマイグレーションもそこから生成される。手動同期が消える
  2. D1ネイティブサポート: drizzle-orm/d1が公式で提供されており、@atinuxの薄いラッパーに頼る必要がない
  3. 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レビューで穴を塞いでから保存するという流れを、今回初めて一連のパイプラインとして完走した。明日からはこの計画に沿って、実際にスキーマ定義の移植から手を付けていく。