Turso の DB を drizzle でマイグレーションやってみる。
これをやってみます。
prisma + turso のマイグレーションが現状手動でSQLを実行するスタイルで微妙なので drizzle だとどうなるか。
練習用のディレクトリ掘ります。
mkdir turso-drizzle-migration-example
cd turso-drizzle-migration-example/
pnpm init します。
$ pnpm init
Wrote to /Users/coji/progs/spike/turso/turso-drizzle-migration-example/package.json
{
"name": "turso-drizzle-migration-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
turso のサインアップは済んでるので以下はやらない。
turso auth signup
テスト用のDBを作成。
$ turso db create drizzle-turso-db
Created database drizzle-turso-db at group default in 4.024s.
Start an interactive SQL shell with:
turso db shell drizzle-turso-db
To see information about the database, including a connection URL, run:
turso db show drizzle-turso-db
To get an authentication token for the database, run:
turso db tokens create drizzle-turso-db
DBの情報表示。
turso db show drizzle-turso-db
Name: drizzle-turso-db
URL: libsql://drizzle-turso-db-coji.turso.io
ID: ce5baf12-3212-4a87-84cc-fa3793daa931
Group: default
Version: 0.23.7
Locations: bos, cdg, nrt
Size: 4.1 kB
Sleeping: No
Bytes Synced: 0 B
Database Instances:
NAME TYPE LOCATION
nrt primary nrt
cdg replica cdg
bos replica bos
トークンつくります。
$ turso db tokens create drizzle-turso-db
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
.env を書きます
TURSO_CONNECTION_URL=libsql://drizzle-turso-db-coji.turso.io
TURSO_AUTH_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
drizzle 関係の npm module を入れます。
$ pnpm add drizzle-orm @libsql/client
$ pnpm add -D drizzle-kit
Packages: +32
++++++++++++++++++++++++++++++++
Progress: resolved 38, reused 31, downloaded 1, added 32, done
dependencies:
+ @libsql/client 0.5.6
+ drizzle-orm 0.30.2
Done in 6.4s
Packages: +60
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 141, reused 76, downloaded 16, added 60, done
devDependencies:
+ drizzle-kit 0.20.14
Done in 5.1s
drizzle のインスタンス作るヘルパーですね。
mkdir -p src/db
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
const client = createClient({
url: process.env.TURSO_CONNECTION_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
export const db = drizzle(client);
次に drizzle.config.ts。
import 'dotenv/config';
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema.ts',
out: './migrations',
driver: 'turso',
dbCredentials: {
url: process.env.TURSO_CONNECTION_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
},
} satisfies Config;
これは、Drizzle Kit というものによる、マイグレーション用の設定のようです。
スキーマの定義を書きます。ああ、こうなるのかあ。
import { sql } from 'drizzle-orm';
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: integer('id').primaryKey(),
name: text('name').notNull(),
email: text('email').unique().notNull(),
});
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
createdAt: text('created_at')
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
});
export type InsertUser = typeof users.$inferInsert;
export type SelectUser = typeof users.$inferSelect;
export type InsertPost = typeof posts.$inferInsert;
export type SelectPost = typeof posts.$inferSelect;
これでマイグレーションを作成して、DBに apply するそうです。
npx drizzle-kit generate:sqlite
drizzle-kit: v0.20.14
drizzle-orm: v0.30.2
No config path provided, using default 'drizzle.config.ts'
Reading config file '/Users/coji/progs/spike/turso/turso-drizzle-migration-example/drizzle.config.ts'
node:internal/modules/cjs/loader:1144
const err = new Error(message);
^
Error: Cannot find module 'dotenv/config'
Require stack:
- /Users/coji/progs/spike/turso/turso-drizzle-migration-example/drizzle.config.ts
- /Users/coji/progs/spike/turso/turso-drizzle-migration-example/node_modules/.pnpm/drizzle-kit@0.20.14/node_modules/drizzle-kit/bin.cjs
at Module._resolveFilename (node:internal/modules/cjs/loader:1144:15)
at Module._load (node:internal/modules/cjs/loader:985:27)
at Module.require (node:internal/modules/cjs/loader:1235:19)
at require (node:internal/modules/helpers:176:18)
at Object.<anonymous> (/Users/coji/progs/spike/turso/turso-drizzle-migration-example/drizzle.config.ts:1:8)
at Module._compile (node:internal/modules/cjs/loader:1376:14)
at Module._compile (/Users/coji/progs/spike/turso/turso-drizzle-migration-example/node_modules/.pnpm/drizzle-kit@0.20.14/node_modules/drizzle-kit/bin.cjs:8644:30)
at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)
at Object.newLoader [as .ts] (/Users/coji/progs/spike/turso/turso-drizzle-migration-example/node_modules/.pnpm/drizzle-kit@0.20.14/node_modules/drizzle-kit/bin.cjs:8648:13)
at Module.load (node:internal/modules/cjs/loader:1207:32) {
code: 'MODULE_NOT_FOUND',
requireStack: [
'/Users/coji/progs/spike/turso/turso-drizzle-migration-example/drizzle.config.ts',
'/Users/coji/progs/spike/turso/turso-drizzle-migration-example/node_modules/.pnpm/drizzle-kit@0.20.14/node_modules/drizzle-kit/bin.cjs'
]
}
Node.js v20.11.1
あかん
cjs なのが気になったので package.json に "type": "module", 足して esm にします。
{
"name": "turso-drizzle-migration-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
+ "type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@libsql/client": "^0.5.6",
"drizzle-orm": "^0.30.2"
},
"devDependencies": {
"drizzle-kit": "^0.20.14"
}
}
dotenv 入れてなかったのでいれます。
$ pnpm i dotenv
Packages: +1
+
Progress: resolved 142, reused 93, downloaded 0, added 0, done
dependencies:
+ dotenv 16.4.5
Done in 560ms
もっかい drizzle-kit generate:sqlite します。
$ npx drizzle-kit generate:sqlite
drizzle-kit: v0.20.14
drizzle-orm: v0.30.2
No config path provided, using default 'drizzle.config.ts'
Reading config file '/Users/coji/progs/spike/turso/turso-drizzle-migration-example/drizzle.config.ts'
2 tables
posts 5 columns 0 indexes 1 fks
users 3 columns 1 indexes 0 fks
[✓] Your SQL migration file ➜ migrations/0000_optimal_overlord.sql 🚀
なんかできたっぽい。
できた migration 用の SQL ファイル。
CREATE TABLE `posts` (
`id` integer PRIMARY KEY NOT NULL,
`title` text NOT NULL,
`content` text NOT NULL,
`user_id` integer NOT NULL,
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `users` (
`id` integer PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`email` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
これではまだ DB には反映されてないみたい。一応確認する。
$ turso db shell drizzle-turso-db
Connected to drizzle-turso-db at libsql://drizzle-turso-db-coji.turso.io
Welcome to Turso SQL shell!
Type ".quit" to exit the shell and ".help" to list all available commands.
→ .tables
→ .schema
→ .quit
確かに、空っぽのままですね。
マイグレーションを実行するには drizzle の migrate 関数を実行するそうです。
この関数は DB 接続と、マイグレーションファイルの場所を指定して、実行するとまだ適用されていないマイグレーションSQLをあててくれるそうな。
で、その migrate 関数を実行するコードを src/db/index.ts
に置くと。
import 'dotenv/config';
import { resolve } from 'node:path';
import { db } from './db/db';
import { migrate } from 'drizzle-orm/libsql/migrator';
(async () => {
await migrate(db, { migrationsFolder: resolve(__dirname, '../migrations') });
})();
そもそも typescript 入れてなかったので入れよう。実行用に tsx も入れとく。
$ pnpm i -D typescript tsx
Packages: +3
+++
Progress: resolved 145, reused 96, downloaded 0, added 3, done
devDependencies:
+ tsx 4.7.1
+ typescript 5.4.2
Done in 1.3s
tsconfig.jsonを作る。一旦初期で。
$ pnpm tsc --init
Created a new tsconfig.json with:
TS
target: es2016
module: commonjs
strict: true
esModuleInterop: true
skipLibCheck: true
forceConsistentCasingInFileNames: true
You can learn more at https://aka.ms/tsconfig
とりあえずこんな感じにしておく。
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
現時点のディレクトリツリー。
.
├── drizzle.config.ts
├── migrations
│ ├── 0000_optimal_overlord.sql
│ └── meta
│ ├── 0000_snapshot.json
│ └── _journal.json
├── package.json
├── pnpm-lock.yaml
├── src
│ ├── index.ts
│ └── db
│ ├── db.ts
│ └── schema.ts
└── tsconfig.json
マイグレーションの名前 (今回は optimal_overload
) ってどうやって指定するのかな。
drizzle-kit generate:sqlite のヘルプを見てみる。ていうかなんで :sqlite って毎回指定しないといけないんだろう
npx drizzle-kit generate:sqlite --help
Usage: bin generate:sqlite [options]
Options:
--schema <schema...> Path to a schema file or folder
--out <out> Output folder, 'drizzle' by default
--breakpoints Prepare SQL statements with breakpoints
--custom Prepare empty migration file for custom SQL
--config <config> Path to a config.json file, drizzle.config.ts by default
-h, --help display
うーん、指定できなそう?
drizzle-kit のヘルプを見てみよう。
npx drizzle-kit --help
Usage: bin [options] [command]
Options:
--version, -v output the version number
-h, --help display help for command
Commands:
generate:pg [options]
generate:mysql [options]
generate:sqlite [options]
check:pg [options]
check:sqlite [options]
up:pg [options]
up:mysql [options]
up:sqlite [options]
introspect:pg [options]
introspect:mysql [options]
drop [options]
push:mysql [options]
introspect:sqlite [options]
push:sqlite [options]
check:mysql [options]
push:pg [options]
studio [options]
help [command] display help for command
なぜかコマンドの並びがソートされてないのでソート眺める
check:mysql [options]
check:pg [options]
check:sqlite [options]
drop [options]
generate:mysql [options]
generate:pg [options]
generate:sqlite [options]
help [command] display help for command
introspect:mysql [options]
introspect:pg [options]
introspect:sqlite [options]
push:mysql [options]
push:pg [options]
push:sqlite [options]
studio [options]
up:mysql [options]
up:pg [options]
up:sqlite [options]
うーん、push は想像つくけど、up ってなんだろう。まあいいや。
そもそも db に apply させる処理はどうやってやるの。 src/index.ts
を自分で実行するのかな?なんで、index.ts なんだ。。
てか、このチュートリアルでは以下の様に書いてあるから、src/index.ts
なのは普通ではないのだろう。
To run migrations, use the migrate function. This function takes your database connection and the path to your migrations directory as arguments. In the provided example, we call it in index.ts, applying all pending migrations to the database.
日本語訳
マイグレーションを実行するには、migrate関数を使います。この関数はデータベース接続と migrations ディレクトリへのパスを引数として受け取ります。提供されている例では、index.tsの中でこの関数を呼び出し、保留中のすべてのマイグレーションをデータベースに適用しています。
The basic file structure outlines how the project is organized, with the src directory containing your database and schema definitions (db.ts and schema.ts) and the entry point to your application (index.ts). The migrations directory holds your migrations and their metadata.
日本語訳
基本的なファイル構造はプロジェクトがどのように構成されているかを示しており、srcディレクトリにはデータベースとスキーマの定義(db.tsとschema.ts)とアプリケーションのエントリポイント(index.ts)があります。migrationsディレクトリには、マイグレーションとそのメタデータが格納されています。
ディレクトリ構造の説明したあとで以下のように急に書いてある。alternative のまえに、普通はどうするのか知りたい
As an alternative to generating migration files, you can apply your schema changes directly to the database without generating any migrations files using drizzle-kit push:sqlite command:
日本語訳
マイグレーションファイルを生成する代わりに、drizzle-kit push:sqliteコマンドを使用して、マイグレーションファイルを生成せずにスキーマの変更をデータベースに直接適用することができます:
npx drizzle-kit push:sqlite
It is good for situations where you need to quickly test new schema designs or changes in a local development environment, allowing for fast iterations without the overhead of managing migration files.
日本語訳
ローカルの開発環境で新しいスキーマ設計や変更を素早くテストする必要がある場合に適しており、マイグレーションファイルの管理にかかるオーバーヘッドなしに、迅速な反復を可能にする。
とりあえず何も考えずに migrate 関数が書かれた src/index.ts
を実行してみよう。
$ pnpm tsx src/index.ts
/Users/coji/progs/spike/turso/turso-drizzle-migration-example/src/index.ts:6
await migrate(db, { migrationsFolder: resolve(__dirname, "../migrations") });
^
ReferenceError: __dirname is not defined
at <anonymous> (/Users/coji/progs/spike/turso/turso-drizzle-migration-example/src/index.ts:6:49)
at <anonymous> (/Users/coji/progs/spike/turso/turso-drizzle-migration-example/src/index.ts:7:1)
at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
at async loadESM (node:internal/process/esm_loader:28:7)
at async handleMainPromise (node:internal/modules/run_main:113:12)
Node.js v20.11.1
おっと、ESM だ。
__dirname を用意して top-level await に。
import "dotenv/config";
import { resolve, dirname } from "node:path";
import { db } from "./db/db";
import { migrate } from "drizzle-orm/libsql/migrator";
+const __dirname = dirname(new URL(import.meta.url).pathname);
-(async () => {
await migrate(db, { migrationsFolder: resolve(__dirname, "../migrations") });
-})();
DB の中身を確認してみる。
$ turso db shell drizzle-turso-db
Connected to drizzle-turso-db at libsql://drizzle-turso-db-coji.turso.io
Welcome to Turso SQL shell!
Type ".quit" to exit the shell and ".help" to list all available commands.
→ .schema
CREATE TABLE "__drizzle_migrations" (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at numeric
);
CREATE TABLE `posts` (
`id` integer PRIMARY KEY NOT NULL,
`title` text NOT NULL,
`content` text NOT NULL,
`user_id` integer NOT NULL,
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
CREATE TABLE `users` (
`id` integer PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`email` text NOT NULL
);
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
おお、適用されている。
けど、migrate ってなにも表示されないのw
__drizzle_migrations の中身
→ select * from __drizzle_migrations;
ID HASH CREATED AT
NULL 4bef5132616629fad7fc94e9db67b5bd0feeb3f3ea08793668c3ed4691a58b1d 1710654206169
created_at unix シリアルみたいだなあ。prisma と一緒か。。
てか ID が NULL だけどいいんすか。
drizzle-kit push:sqlite をやってみたい。
src/db/schema.ts
を編集して、 users
テーブルに photoUrl
カラムを足してみよう。
import { sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: integer("id").primaryKey(),
name: text("name").notNull(),
email: text("email").unique().notNull(),
+ photoUrl: text("photo_url"),
});
export const posts = sqliteTable("posts", {
id: integer("id").primaryKey(),
title: text("title").notNull(),
content: text("content").notNull(),
userId: integer("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
createdAt: text("created_at")
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
});
export type InsertUser = typeof users.$inferInsert;
export type SelectUser = typeof users.$inferSelect;
export type InsertPost = typeof posts.$inferInsert;
export type SelectPost = typeof posts.$inferSelect;
push してみる。
$ npx drizzle-kit push:sqlite
No config path provided, using default path
Reading config file '/Users/coji/progs/spike/turso/turso-drizzle-migration-example/drizzle.config.ts'
drizzle-kit: v0.20.14
drizzle-orm: v0.30.2
Warning Found data-loss statements:
· You're about to delete __drizzle_migrations table with 1 items
THIS ACTION WILL CAUSE DATA LOSS AND CANNOT BE REVERTED
Do you still want to push changes?
❯ No, abort
Yes, I want to remove 1 table
デフォ指定がないから削除するってでるのかね。
デフォ指定入れてやってみる
default NULL に。これでいいのかしら。
import { sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: integer("id").primaryKey(),
name: text("name").notNull(),
email: text("email").unique().notNull(),
- photoUrl: text("photo_url"),
+ photoUrl: text("photo_url").default(sql`NULL`),
});
export const posts = sqliteTable("posts", {
id: integer("id").primaryKey(),
title: text("title").notNull(),
content: text("content").notNull(),
userId: integer("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
createdAt: text("created_at")
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
});
export type InsertUser = typeof users.$inferInsert;
export type SelectUser = typeof users.$inferSelect;
export type InsertPost = typeof posts.$inferInsert;
export type SelectPost = typeof posts.$inferSelect;
もっかい。
$ npx drizzle-kit push:sqlite
No config path provided, using default path
Reading config file '/Users/coji/progs/spike/turso/turso-drizzle-migration-example/drizzle.config.ts'
drizzle-kit: v0.20.14
drizzle-orm: v0.30.2
Warning Found data-loss statements:
· You're about to delete __drizzle_migrations table with 1 items
THIS ACTION WILL CAUSE DATA LOSS AND CANNOT BE REVERTED
Do you still want to push changes?
❯ No, abort
Yes, I want to remove 1 table
あ、削除されるの __drizzle_migrations テーブルじゃん。あれ、これいいの消して。
何も考えずに一旦 Yes
[✓] Changes applied
確認。たしかに消えたね。users.photoUrl
カラムはデフォ NULL で増えた。
$ turso db shell drizzle-turso-db
Connected to drizzle-turso-db at libsql://drizzle-turso-db-coji.turso.io
Welcome to Turso SQL shell!
Type ".quit" to exit the shell and ".help" to list all available commands.
→ .schema
CREATE TABLE `posts` (
`id` integer PRIMARY KEY NOT NULL,
`title` text NOT NULL,
`content` text NOT NULL,
`user_id` integer NOT NULL,
`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
CREATE TABLE `users` (
`id` integer PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`email` text NOT NULL
, `photo_url` text DEFAULT NULL);
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
一旦ここまででマイグレーションは一通りかな?
所感。
good
- migrations SQLが残って、未適用のマイグレーションをSQLファイル指定せずにあてられるのは嬉しい (vs prisma+turso / kysely)
- エンティティの型が inferInsert / inferSelect できれいに取り出せるのは良い (vs kysely)
no good
- migration 用の cli がないのがダルい。(vs prisma)
- マイグレーションをあてるのを flyio で本番デプロイ時にやる、みたいのは面倒そう (dockerイメージを小さくするため / メモリ消費を小さくするため、tsx, ts などを入れないで済むほうが楽だから)
- エンティティの型名が SelectUser とかになるのがちょっとダルい (vs prisma)
総じて、turso を使う前提だと以下の順になりそう。僅差だけどなあ。
- drizzle
- prisma+kysely+kysely-libsql (マイグレーション管理がダルい)
- prisma+turso EA (マイグレーション管理がダルい)
flyio で使うなら:
3. prisma+turso EA (やっぱ prisma が楽)
cloudflare pages で使うなら:
- drizzle OR 2. prisma+kysely+kysely-libsql (prisma は 1MB 制限に対してデカすぎ)
prisma.schema はやっぱり便利なんだよなあ。慣れてるし、仕事でやってるクライアントの案件でも使ってて剥がすのはちょっと想像つかない。
drizzle のマイグレーション管理は良いんだけど、スキーマ定義と migration の CLI がないので、慣れるのに気合がいるなあ。
ORM のクエリーの書きやすさについては、あんまりこだわりがなかったりします。
型がついてくれて、柔軟にできるのがいいけれど、フロント用だと別に prisma でも十分だし。
管理系の集計クエリなどには prisma だときついので kysely か drizzle か、ってかんじ。
決定的なのがないなあ。
今回作業した結果のリポジトリ。