Deno + Pglite + Drizzle で依存の少ないDBアプリを作る
CI まで一式動いてるのがここ
pglite は postgres を wasm コンパイルしたもの。
これを deno + drizzle からマイグレーションして叩く。
なぜこの組み合わせか
ローカルにAIエージェント用の簡単なDBツールを量産したかった。deno でスクリプトを書きまくってるので、 deno を前提に色々試した。
色々試したのだが、最終的に Pglite で Postgres を叩くことにした。インストールが不要で、DB周りのセットアップが一番手数が少ない。手数の少なさを最重要とした。
最低限これだけでいい。
import { PGlite } from "npm:@electric-sql/pglite";
const db = new PGlite(); // `{dataDir: ...}` で初期化パスを渡せる
await db.exec("create table test (id serial primary key, name text);");
await db.exec("insert into test (name) values ('Alice');");
await db.query("select * from test;");
出力に関してはシングルファイルを吐く sqlite のほうがポータブルではあるのだが、deno の node:sqlite
互換実装が追加されたのがつい先日、かつ各種ORMのアダプタが現状皆無なので取り回しが難しかった。
結局 postgres のアダプタなので、好きな時に好みの DBaaS をバックエンドにできる。
Prismaでない理由は、 deno で prisma 動かすのが面倒であり、prisma 自体が今 TS に書き直されてる最中なので、それが終わったら大幅に変わるのが予想される。今は時期が悪い。
DBを初期化する
やっていく
$ deno init
$ deno add npm:drizzle-kit npm:drizzle-orm
こういう状態を作る。
./db
├── client.ts
├── migrate.ts
├── migrations
│ ├── 0000_fearless_shotgun.sql
│ └── meta
│ ├── 0000_snapshot.json
│ └── _journal.json
└── schema.ts
deno.json
drizzle.config.ts
スキーマファイルを置く。
// なんでもいいからテーブル定義
import { integer, pgTable, varchar } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }).notNull(),
age: integer().notNull(),
});
import { PGlite } from "npm:@electric-sql/pglite";
import { drizzle } from "drizzle-orm/pglite";
const client = new PGlite({
dataDir: "./data",
});
export const db = drizzle(client);
drizzle.config.ts は drizzle-kit
に認識させるためにこの名前である必要がある。
import { defineConfig } from "drizzle-kit";
export default defineConfig({
out: "./db/migrations",
schema: "./db/schema.ts",
dialect: "postgresql",
dbCredentials: {
url: "",
},
});
pglite を使う場合、 url で "" を渡すのがミソ。
この状態で drizzle-kit でマイグレーションファイルを生成。
$ deno run -A npm:drizzle-kit generate
No config path provided, using default 'drizzle.config.ts'
Reading config file '/home/mizchi/todo/drizzle.config.ts'
1 tables
users 3 columns 0 indexes 0 fks
[✓] Your SQL migration file ➜ db/migrations/0000_bored_giant_man.sql 🚀
## 確認
$ tree migrations
migrations
├── 0000_tearful_the_phantom.sql
└── meta
├── 0000_snapshot.json
└── _journal.json
ここから、pglite ではなく通常の posgres
の場合、 drizzle-kit migrate
を行うのだが、pglite の場合、 drizzle-kit/pglite/migrator
を使う必要がある。
こういうスクリプトを用意する。
import { migrate } from "drizzle-orm/pglite/migrator";
import { db } from "./client.ts";
const folderPath = new URL("./migrations", import.meta.url).pathname;
await migrate(db, {
migrationsFolder: folderPath,
});
console.log("Migration complete");
実行
$ deno run -A db/migrate.ts
Migration complete
drizzle client から実行
import { db } from "./db/client.ts";
import { users } from "./db/schema.ts";
const ret = await db.select().from(users);
console.log(ret); // []
これで動作確認できた
DBスキーマを更新する
deno.json にタスクで書いておく。
{
"nodeModulesDir": "auto",
"tasks": {
"db-up": "deno run -A npm:drizzle-kit generate",
"db-migrate": "deno run -A db/migrate.ts"
},
"imports": {
"drizzle-kit": "npm:drizzle-kit@^0.30.5",
"drizzle-orm": "npm:drizzle-orm@^0.40.0"
}
}
これで、 deno task db-up
deno task db-migrate
が叩ける。
-
db/schema.ts
でスキーマを変更 -
deno task db-up
でマイグレーションを生成 -
db/migartions/*.sql
を確認 -
deno task db-migarte
で適用
このワークフローで回せる。
インメモリDBのテストコードを書く
せっかくポータブルな pglite を使ってるので、これを使ってテストを書く。
new PGlite()
するときにインメモリでDBを作れるので、テストを簡単に作れる。
import { migrate } from "drizzle-orm/pglite/migrator";
import { PGlite } from "npm:@electric-sql/pglite";
import { drizzle } from "drizzle-orm/pglite";
import { beforeAll, afterAll, test } from "jsr:@std/testing/bdd";
import { configureGlobalSanitizers } from "jsr:@std/testing/unstable-bdd";
import { expect } from "jsr:@std/expect";
import { users } from "./db/schema.ts";
import { eq } from "drizzle-orm";
// NOTE: drizzle leaks the global sanitizers, so we need to disable them
configureGlobalSanitizers({
sanitizeOps: false,
});
const client = new PGlite();
const db = drizzle(client);
beforeAll(async () => {
await migrate(db, {
migrationsFolder: new URL("./db/migrations", import.meta.url).pathname,
});
});
afterAll(async () => {
await client.close();
});
test("CRUD", async () => {
// Create
await db.insert(users).values({
name: "John",
age: 30,
});
const ret = await db.select().from(users);
// Read
expect(ret).toEqual([{ name: "John", age: 30, id: 1 }]);
await db.update(users).set({ age: 31 }).where(eq(users.id, 1));
// Update
const ret2 = await db.select().from(users);
expect(ret2).toEqual([{ name: "John", age: 31, id: 1 }]);
// Delete
await db.delete(users).where(eq(users.id, 1));
const ret3 = await db.select().from(users);
expect(ret3).toEqual([]);
});
実行
$ deno test -A run.test.ts
running 1 test from ./run.test.ts
global ...
CRUD ... ok (6ms)
global ... ok (1s)
ok | 1 passed (1 step) | 0 failed (1s)
TODO
これを deno deploy か cloudflare workers にデプロイするフローを作る。
参考
Discussion