Drizzle のセットアップ

Hono REST、Hono GraphQL、Full-Stack、Monorepo の各テンプレートは、SQLite (@libsql/client 経由) + Drizzle ORM を同梱しています。このレシピでは、スキーマ変更、マイグレーション生成、本番環境向けに Turso に向けるといった 2 日目以降のフローを扱います。

提供されるもの

  • src/Schema.res@module("drizzle-orm/sqlite-core") バインディングによる Drizzle テーブル定義

  • src/Db.rescreateClient({url: ...}) + drizzle(client) + クエリヘルパー

  • drizzle.config.ts — コンパイル済み Schema.res.mjs を指す drizzle-kit の設定ファイル

  • data/app.db — デフォルトのローカル SQLite ファイル (gitignore 済み)

  • スクリプト — db:generatedb:migrate

カラムの追加

src/Schema.res を編集します:

let users = sqliteTable("users", {
  "id": intCol("id", {"primaryKey": true, "autoIncrement": true}),
  "name": textCol("name", {"notNull": true}),
  "email": textCol("email", {"notNull": true}),
  "createdAt": intCol("created_at", {"notNull": true, "default": Date.now()}),
})

再生成して適用します:

pnpm res:build     # refresh Schema.res.mjs that drizzle-kit reads
pnpm db:generate   # emit migration SQL under ./drizzle/
pnpm db:migrate    # apply pending migrations

コラボレーターが同じ変更を再生できるよう、./drizzle/ に生成された SQL をコミットしてください。

テーブルの追加

let posts = sqliteTable("posts", {
  "id": intCol("id", {"primaryKey": true, "autoIncrement": true}),
  "title": textCol("title", {"notNull": true}),
  "authorId": intCol("author_id", {"notNull": true}),
})

同じ 2 ステップ: pnpm db:generate の後に pnpm db:migrate

クエリ

生成された src/Db.res は必要な主要動詞をバインドしています。さらに追加する場合は @send を使います:

@send external where: ('builder, 'expr) => 'builder = "where"
@send external eq: ('col, 'value) => 'expr = "eq"
@send external limit: ('q, int) => 'q = "limit"

例 — id で検索する:

let findUser = async id => {
  let rows =
    await Db.db
    ->Db.select({
      "id": Schema.users["id"],
      "name": Schema.users["name"],
    })
    ->Db.from(Schema.users)
    ->Db.where(Db.eq(Schema.users["id"], id))
    ->Db.limit(1)
    ->Db.allAsync
  rows->Array.get(0)
}

Turso へ接続する (本番環境)

Turso はマネージド libsql ホストです — リモート URL に対しても同じクライアントが動作します。

  1. brew install tursodatabase/tap/turso

  2. turso auth login

  3. turso db create my-app

  4. turso db tokens create my-app

  5. 環境変数を設定してマイグレーションを実行します:

export DATABASE_URL="libsql://<db-name>-<org>.turso.io"
export DATABASE_AUTH_TOKEN="<token-from-step-4>"
pnpm db:migrate

src/Db.res を拡張して認証トークンを読み込みます:

let authToken =
  processEnv
  ->Dict.get("DATABASE_AUTH_TOKEN")
  ->Option.getOr("")

let client = createClient({"url": dbUrl, "authToken": authToken})

Drizzle 設定に関する注意

drizzle.config.tsコンパイル済みsrc/Schema.res.mjs を読み込みます。スキーマ編集後にマイグレーションが空になる場合は、ReScript がコンパイルされていることを確認してください:

pnpm res:build && pnpm db:generate

あるいは別のターミナルで pnpm res:dev を実行して、Schema.res.mjs を常に最新に保ちます。

方言の切り替え

PostgreSQL を使用する場合は、以下を置き換えます:

  • @libsql/clientpostgres (または pg)

  • drizzle-orm/libsqldrizzle-orm/postgres-js

  • drizzle-orm/sqlite-coredrizzle-orm/pg-core

  • drizzle.config.tsdialect: "sqlite"dialect: "postgresql"

クエリヘルパーは多相的なため、ルートモジュール (Routes/Users.res など) は変更不要です。

よくある落とし穴

  • スキーマ編集後に "No migrations pending" が出るdrizzle-kit はコンパイル済みの .mjs を読むため、先に res:build を実行してください。

  • ウォッチモードとのロック競合 — Windows でファイルロックエラーが出る場合、db:migrate の前に res:dev を停止してください。

  • 外部キー制約 — SQLite はデフォルトでは外部キー制約を強制しません。必要な場合は接続時に PRAGMA foreign_keys = ON; を実行してください。