TypeScript + Bun + SQLite + Drizzle ORM な環境にて、Drizzle Kit の各コマンドを試してみた

TypeScript + Bun な環境にて、SQLiteを操作したいことがありました。

Bunにはネイティブの SQLite driver があることから、そのまま bun:sqlite を使うこともできそうでした。
SQLite – API | Bun Docs

ただ、日頃ORMでDBまわりを書いていることから、ORM的な何かを使いたくなりました。

 
BunのExamplesを見ていたところ、 Drizzle ORM が紹介されていました。

 
また、Drizzleでは

Drizzle Kit — is a CLI companion for automatic SQL migrations generation and rapid prototyping.

 

Drizzle ORM - Overview

と、 Drizzle Kit を使ってマイグレーションなどを行うことができそうでした。

 
他にも、Drizzleを使っている日本語の記事を読んだところ、自分が欲しいものとしてちょうど良さそうと感じました。

 
そこで、まずは Drizzle Kit の各コマンドをためしてみたときのメモを残します。

 
目次

 

環境

  • Windows11 WSL2
  • TypeScript 5.4.5
    • tsc -v より
  • Bun 1.1.6
  • DBは SQLite
    • bun:sqlite モジュールを使う
  • Drizzle ORM 0.30.9
  • Drizzle Kit 0.20.17
  • WebStorm 2024.1.1

 
なお、WebStorm 2024.1.1 では、まだ Bun を公式サポートしていないようです。以下のissueによると、デバッグまわりが厳しそうな印象です。

 
ただ、今回の記事ではデバッグは不要なので、WebStormで実装を進めることにします。

 

Bun 上で Drizzle Kit の各コマンドを実行するための準備

Bunのセットアップ

まだ何も Bun の環境ができていないことから、最初に Bun をセットアップします。

Bunの公式ドキュメントに従い、Bunをインストールします。
Installing | Installation | Bun Docs

今回は nodenv を使ってリポジトリごとに Node.js を使い分けている環境だったので、 npm でインストールします。

$ npm install -g bun

 
次に bun --version したところ、bun コマンドがないと言われてしまいました。

そこで、Bunの公式ドキュメントのHow to add to your PATH) に従い、追加で設定作業を行います。

まずは shell を確認します。

$ echo $SHELL
/bin/bash

 
bashだったので、 ~/.bashrc にの末尾に以下を追記します。

export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"

 
新しくターミナルを開いたところ、 bun コマンドを実行できました。

$ bun --version
1.1.6

 
Bunの設定が完了したため、 bun init を実行し、projectの設定をしておきます。
bun init – Templating | Bun Docs

$ bun init

bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit

package name (drizzle_with_bun):
entry point (index.ts):

Done! A package.json file was saved in the current directory.
 + index.ts
 + .gitignore
 + tsconfig.json (for editor auto-complete)
 + README.md

To get started, run:
  bun run index.ts

 

Drizzleのセットアップ

Bunの環境ができたので、次は Drizzle ORM や Drizzle Kit のセットアップを行います。

 

必要なパッケージのインストール

Drizzle の公式ドキュメントに従い、Bun SQLite を使うために必要なパッケージをインストールします。
Bun SQLite | Drizzle ORM - SQLite

まずは Drizzle ORMをインストールします。

$ bun add drizzle-orm
bun add v1.1.6 (e58d67b4)

 installed drizzle-orm@0.30.9

 1 package installed [734.00ms]

 
続いて Drizzle Kitを -D オプション付きでインストールします。

$ bun add -D drizzle-kit
bun add v1.1.6 (e58d67b4)

 installed drizzle-kit@0.20.17 with binaries:
  - drizzle-kit

 62 packages installed [3.02s]

 

drizzle.config.ts の作成

Drizzle ORMでは drizzle.config.ts ファイルにて設定を変更できます。
Drizzle ORM - Configuration

今回のDrizzle ORM の設定は

とするため、以下の内容で drizzle.config.ts を作成します。

import type { Config } from "drizzle-kit"

export default {
  schema: "./src/schema/*",
  out: "./drizzle",
} satisfies Config

 
以上で準備は終わりです。

 

drizzle-kit generate でマイグレーションファイルを作成

まずは drizzle-kit generateマイグレーションファイルを生成してみます。
Drizzle ORM - Migrations

ドキュメントによると、PythonDjangoのように、スキーマを元にマイグレーションSQLを自動で生成するようです。

実際にためしてみます。

 

スキーマファイルを作成

マイグレーションファイルの元となるスキーマファイルを作成します。

今回用意するスキーマ

  • テーブル名 authors
  • 列は以下の2つ
    • id

      • integer型
      • 主キーで、自動インクリメント
    • name

      • string型

とし、 src/schema/authors.ts へ定義します。

ちなみに、今回の定義では以下の点を考慮しています。

 
実際の定義はこちら。

import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core'

export const authors = sqliteTable("authors", {
  id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }),
  name: text("name"),
})

 

package.json に Drizzle Kit の generate コマンドを追加

コマンドは何度も使うことになりそうなので、 package.json へコマンドを追加しておきます。

"scripts": {
  "generate": "drizzle-kit generate:sqlite"
},

 

マイグレーションファイルを自動生成

続いて、 Drizzle Kit によるマイグレーションファイルの生成を行います。

Bunでは bun run により package.jsonscripts を実行できます。
Run a package.json script | bun run – Runtime | Bun Docs

そこで、先ほど追加した generate を実行してみます。

# 実行
$ bun run generate

# 以下はログ
$ drizzle-kit generate:sqlite
drizzle-kit: v0.20.17
drizzle-orm: v0.30.9

No config path provided, using default 'drizzle.config.ts'
Reading config file 'path/to/drizzle.config.ts'
1 tables
authors 2 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ drizzle/0000_yielding_the_spike.sql 🚀

 
生成されたファイル 0000_yielding_the_spike.sql を確認すると、以下のSQLが定義されていました。

CREATE TABLE `authors` (
    `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
    `name` text
);

 
また、 meta ディレクトリの中にもファイルが生成されていました。

drizzle_with_bun/drizzle$ tree
.
├── 0000_yielding_the_spike.sql
└── meta
    ├── 0000_snapshot.json
    └── _journal.json

 

もう1つマイグレーションファイルを追加

次に、Drizzle Kit がスキーマの差分をうまく検知できるか、確認してみます。

先ほど作成したスキーマ authors.tsage 列を追加します。

export const authors = sqliteTable("authors", {
  id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }),
  name: text("name"),
  age: integer("age"),  // 追加
})

 
再度 bun run generate したところ、新しくファイルが生成されました。

$ bun run generate

$ drizzle-kit generate:sqlite
drizzle-kit: v0.20.17
drizzle-orm: v0.30.9

No config path provided, using default 'drizzle.config.ts'
Reading config file '/home/thinkami/dev/projects/typescript/drizzle/drizzle_with_bun/drizzle.config.ts'
1 tables
authors 3 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ drizzle/0001_flawless_hitman.sql 🚀

 
生成された 0001_flawless_hitman.sql を確認すると、以下のSQLが生成されていました。差分が検知できているようです。

ALTER TABLE authors ADD `age` integer;

 
また、 metaディレクトリの中にも変化があり、

  • 0001_snapshot.json ファイルが追加
  • _journal.json ファイルの entries に要素が追加

となっていました。

 

migtate.ts を作成し、SQLiteマイグレーションファイルを適用

ここまででマイグレーションファイルを2つ作成しました。ただ、まだこれらのファイルはSQLiteへ適用していません。

そこで、これらマイグレーションファイルを適用していきます。

 
ただ、Drizzle ORMの公式ドキュメントには

Drizzle ORM is designed to be an opt-in solution at any point of your development flow. You can either run the generated migrations via Drizzle, or treat them as generic SQL migrations and run them with any other tool.

 
https://orm.drizzle.team/docs/migrations#quick-start

とありました。

Drizzle ORMやKitには、マイグレーションファイルを適用するコマンドが無いようです。

 
そこで、Bunのドキュメントに従い、 migrate.ts ファイルを作成する方針で対応します。
Use Drizzle ORM with Bun | Bun Examples

 

migrate.ts を作成

Bunのドキュメントにある migrate.ts をそのまま流用し、 migrate.ts を作成します。

import { migrate } from "drizzle-orm/bun-sqlite/migrator"

import { drizzle } from "drizzle-orm/bun-sqlite"
import { Database } from "bun:sqlite"

const sqlite = new Database("sqlite.db")
const db = drizzle(sqlite)

// [Ignore] TS80007: await has no effect on the type of this expression.
await migrate(db, { migrationsFolder: "./drizzle" })

 

package.jsonマイグレーション適用コマンドを追加

migrate という名前で scripts にエントリを追加します。

"scripts": {
  "generate": "drizzle-kit generate:sqlite",
  "migrate": "bun run migrate.ts",
},

 

マイグレーションを適用

bun run migrate を実行すると、マイグレーションが適用され、 sqlite.db が追加されました。

WebStorm のプラグイン Database tools and SQL for WebStormsqlite.db を開くと、 authors テーブルができていました。

age 列も追加されていることから、2つのマイグレーションファイルが一気に適用されたようです。

 
また、SQLiteには __drizzle_migrations というテーブルもできていました。

公式ドキュメントによると

By default, all information about executed migrations will be stored in the database inside the __drizzle_migrations table, and for PostgreSQL, inside the drizzle schema. However, you can configure where to store those records.

 
https://orm.drizzle.team/docs/migrations#configurations

とのことです。

__drizzle_migrations テーブルの中身はこんな感じでした。

 

0.30.9 時点ではマイグレーションロールバック機能がない模様

migrateによるマイグレーション適用があるならば、その逆方向であるロールバックもあるのでは...と思い探してみたところ、Github の discussion がありました。
Migrations Rollback · drizzle-team/drizzle-orm · Discussion #1339

上記を読む限り、現時点ではロールバック機能がないようです。

 

drizzle-kit drop の挙動を確認

上記の通り、Drizzle にはマイグレーションロールバック機能はありません。

一方、Drizzle Kitのドキュメントには Drop migration のコマンドがありました。
Drop migration | Drizzle ORM - List of commands

このコマンドを使うと何が起こるのか気になったことから、試してみます。

 

Drop migrationの実行

drizzle-kit drop を実行してみます。

$ drizzle-kit drop
drizzle-kit: v0.20.17
drizzle-orm: v0.30.9

No config path provided, using default 'drizzle.config.ts'
Reading config file '/path/to/drizzle.config.ts'
No config path provided, using default 'drizzle.config.ts'
Reading config file '/path/to/drizzle.config.ts'
Please select migration to drop:
  0000_yielding_the_spike
❯ 0001_flawless_hitman

# 選択後
[✓] 0001_flawless_hitman migration successfully dropped

 
 
実行後に git status を見たところ、以下が差分として出ていました。

マイグレーションファイルの削除の他に、metaディレクトリの中も変更しているようです。

deleted:    drizzle/0001_flawless_hitman.sql
deleted:    drizzle/meta/0001_snapshot.json
modified:   drizzle/meta/_journal.json

 
一方、SQLiteには変更を加えていないようで、

  • __drizzle_migrations テーブルのレコードに変化なし
  • authors テーブルにも age 列が残ったまま

という状態でした。

 
以上より、drizzle-kit drop は「不要なマイグレーションファイルを削除しつつ、自動生成した meta ディレクトリの中身を変更する」と理解しました。

 

Drop migration後に、差分を検知したり元に戻せるか確認

Drop migrationではマイグレーションファイルと実際のDBで差分が生まれてしまいました。

この差分を検知したり、元に戻せるかを確認してみます。

 

drizzle-kit up では差分を検知できない

ドキュメントには

We’re rapidly evolving Drizzle Kit APIs and from time to time there’s a need to upgrade underlying metadata structure.

drizzle-kit up:{dialect} is a utility command to keep all metadata up to date.

 

https://orm.drizzle.team/kit-docs/commands#maintain-stale-metadata

とあり、Drizzle Kit APIの変更によりメタデータが壊れないようにする目的のコマンドのようでした。そのため、今回の目的とは異なりそうです。

 
それでも、 drop 直後の状態(マイグレーションファイルとテーブルの状態が不一致)で、挙動を確認してみます。

$ drizzle-kit up:sqlite
drizzle-kit: v0.20.17
drizzle-orm: v0.30.9

No config path provided, using default 'drizzle.config.ts'
Reading config file '/path/to/drizzle.config.ts'
Everything's fine 🐶🔥

Drizzle Kit のバージョンアップは行っていないこともあり、 Everything's fine でした。

マイグレーションファイルとテーブルの状態が不一致なことは関係ないようです。

 

drizzle-kit check でも差分を検知できない

drizzle-kit check について、公式ドキュメントには

drizzle-kit check:{dialect} is a very powerful tool for you to check consistency of your migrations.

That’s extremely useful when you have multiple people on the project, altering database schema on different branches.

Drizzle Kit will check for all collisions and inconsistencies.

 

https://orm.drizzle.team/kit-docs/commands#check

とありました。

そこで、ここまでの「マイグレーションファイルとテーブルの状態が不一致」という状態でためしてみます。

$ drizzle-kit check:sqlite
drizzle-kit: v0.20.17
drizzle-orm: v0.30.9

No config path provided, using default 'drizzle.config.ts'
Reading config file '/path/to/drizzle.config.ts'
Everything's fine 🐶🔥

Everything's fine と表示されました。Checkコマンド的には問題ないようです。

 

元に戻す方法を探す

ここまでで差分は検知できないようでした。

そこで、差分は検知できなくても元に戻す方法を探してみます。

 

【NG】再度 Generate migrations する

マイグレーションファイルを消してしまったのなら再度作り直せば良いのでは」ということで、再度マイグレーションファイルを生成してみます。

$ bun run generate
$ drizzle-kit generate:sqlite
drizzle-kit: v0.20.17
drizzle-orm: v0.30.9

No config path provided, using default 'drizzle.config.ts'
Reading config file '/path/to/drizzle.config.ts'
1 tables
authors 3 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ drizzle/0001_real_raider.sql 🚀

 
再度マイグレーションファイルが生成されました。ファイル名は異なるものの、Drop migration した内容と一致しています。

ALTER TABLE authors ADD `age` integer;

 
マイグレーションファイルが生成できたので、 migrate してみます。

# 実行
$ bun run migrate

# 以降はログ
$ bun run migrate.ts
1 | import { entityKind } from "./entity.js";
2 | class DrizzleError extends Error {
3 |   static [entityKind] = "DrizzleError";
4 |   constructor({ message, cause }) {
5 |     super(message);
        ^
DrizzleError: Failed to run the query 'ALTER TABLE authors ADD `age` integer;'
      at new DrizzleError (/path/to/node_modules/drizzle-orm/errors.js:5:5)
      at run (/path/to/node_modules/drizzle-orm/sqlite-core/session.js:72:13)
      at migrate (/path/to/node_modules/drizzle-orm/sqlite-core/dialect.js:546:13)
      at migrate (/path/to/node_modules/drizzle-orm/bun-sqlite/migrator.js:4:3)
      at /path/to/migrate.ts:10:7

15 |   logger;
16 |   exec(query) {
17 |     this.client.exec(query);
18 |   }
19 |   prepareQuery(query, fields, executeMethod, isResponseInArrayMode, customResultMapper) {
20 |     const stmt = this.client.prepare(query.sql);
                      ^
SQLiteError: duplicate column name: age
 errno: 1

      at prepare (bun:sqlite:249:19)
      at prepareQuery (/path/to/node_modules/drizzle-orm/bun-sqlite/session.js:20:18)
      at run (/path/to/node_modules/drizzle-orm/sqlite-core/session.js:70:14)
      at migrate (/path/to/node_modules/drizzle-orm/sqlite-core/dialect.js:546:13)
      at migrate (/path/to/node_modules/drizzle-orm/bun-sqlite/migrator.js:4:3)
      at /path/to/migrate.ts:10:7
error: script "migrate" exited with code 1

 
エラーになってしまいました。 SQLiteError: duplicate column name: age とある通り、すでに age 列が存在しているために発生したようです。

 
この方法ではうまくいかないことがわかったので、 Drop migration しておきます。

$ drizzle-kit drop

...
[✓] 0001_real_raider migration successfully dropped

 

【NG】drizzle-kit introspect する

Drizzle Kit には、既存のDBからスキーマを生成する方法もあるようです。
https://orm.drizzle.team/kit-docs/commands#introspect--pull

そこで、 drizzle-kit introspect:sqlite を実行したところ、エラーになりました。

$ drizzle-kit introspect:sqlite

No config path provided, using default path
Reading config file '/path/to/drizzle.config.ts'
 Invalid input  Either "turso", "libsql", "better-sqlite" are available options for "--driver"

 
公式ドキュメントを見たところ、bun:sqlite だけでは introspect コマンドは使えないようで、別途 driver の指定が必要そうでした。
https://orm.drizzle.team/kit-docs/config-reference#driver

 

introspect できるようセットアップする

設定ファイルの変更が必要そうなことから、 drizzle.config.ts

  • driver
  • dbCredentials

の設定を追加します。

export default {
  schema: "./src/schema/*",
  out: "./drizzle",

  // ここから下を追加
  driver: "better-sqlite",
  dbCredentials: {
    url: "./sqlite.db"
  }
} satisfies Config

 
また、公式ドキュメントによると、driverとして better-sqlite を使う場合は better-sqlite3 も必要そうなので追加でインストールします。
https://orm.drizzle.team/docs/get-started-sqlite#better-sqlite3

$ bun add better-sqlite3

 

introspect する

再度 introspect を実行してみたところ、ディレクトdrizzle の下にスキーマファイル schema.ts が生成されました。

中身を見ると、すべてのスキーマが1つのファイルに含まれていました。

import { sqliteTable, AnySQLiteColumn, numeric, text, integer } from "drizzle-orm/sqlite-core"
  import { sql } from "drizzle-orm"

export const drizzleMigrations = sqliteTable("__drizzle_migrations", {
    id: numeric("id").primaryKey(),
    hash: text("hash").notNull(),
    createdAt: numeric("created_at"),
});

export const authors = sqliteTable("authors", {
    id: integer("id").primaryKey({ autoIncrement: true }).notNull(),
    name: text("name"),
    age: integer("age"),
});

 
ただ、

から、この方法で元に戻すのは難しそうでした。

 

【OK】git reset --hard HEAD で戻す

仕方ないので

  • git reset --hard HEAD の実行
  • instrospect 実行時に生成されたファイルを削除

を行い、変更を元に戻しました。

 
見た目場は元に戻りましたが、この後の別の mirgate が成功するか気になりました。

そこで、authors.ts にスキーマの変更を追加し、ためしてみます。

export const authors = sqliteTable("authors", {
  id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }),
  name: text("name"),
  age: integer("age"),
  note: text("note"),  // 追加
})

 
マイグレーションファイルを生成します。

$ bun run generate

# ...
No config path provided, using default 'drizzle.config.ts'
Reading config file '/path/to/drizzle.config.ts'
1 tables
authors 4 columns 0 indexes 0 fks

[✓] Your SQL migration file ➜ drizzle/0002_slow_baron_zemo.sql 🚀

 
migrate します。

$ bun run migrate

 
SQLiteの構造を見ると、 note が追加されていました。問題なさそうです。

 

drizzle-kit studio により、ブラウザでSQLiteの中身を確認する

Drizzle Kit のドキュメントを見たところ、ブラウザでDBの中身を確認する Drizzle Studio 向けのコマンドがありました。

 
ためしてみたところ、エラーになりました。エラーメッセージから、

  • driver
  • dbCredentials

の設定が必要そうと分かりました。

$ drizzle-kit studio
drizzle-kit: v0.20.17
drizzle-orm: v0.30.9

No config path provided, using default path
Reading config file '/path/to/drizzle.config.ts'
 Invalid input  You need to specify a "driver" param in you config. It will help drizzle to know how to query you database. You can read more about drizzle.config: https://orm.drizzle.team/kit-docs/config-reference
 Invalid input  You need to specify a "dbCredentials" param in you config. It will help drizzle to know how to query you database. You can read more about drizzle.config: https://orm.drizzle.team/kit-docs/config-reference

 
そこで、introspect 向けでセットアップしたときと同じ内容を drizzle.config.ts に追加します。

export default {
  schema: "./src/schema/*",
  out: "./drizzle",

  // ここから下を追加
  driver: "better-sqlite",
  dbCredentials: {
    url: "./sqlite.db"
  }
} satisfies Config

 
再度実行したところ、Drizzle Studio が起動しました。

$ drizzle-kit studio
drizzle-kit: v0.20.17
drizzle-orm: v0.30.9

No config path provided, using default path
Reading config file '/path/to/drizzle.config.ts'

[Warning] Drizzle Studio is currently in Beta. If you find anything that is not working as expected or should be improved, feel free to create an issue on GitHub: https://github.com/drizzle-team/drizzle-kit-mirror/issues/new or write to us on Discord: https://discord.gg/WcRKz2FFxN

Drizzle Studio is up and running on https://local.drizzle.studio

 
https://local.drizzle.studio にブラウザでアクセスしたところ、DBの中身が表示されました。

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/drizzle_with_bun-example

今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/drizzle_with_bun-example/pull/1