🐘

Deno + Pglite + Drizzle で依存の少ないDBアプリを作る

2025/02/28に公開

CI まで一式動いてるのがここ

https://github.com/mizchi/deno-drizzle-pglite

pglite は postgres を wasm コンパイルしたもの。

https://pglite.dev/

これを 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のアダプタが現状皆無なので取り回しが難しかった。

https://docs.deno.com/examples/sqlite/

結局 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

スキーマファイルを置く。

db/schema.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(),
});
db/client.ts
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 に認識させるためにこの名前である必要がある。

drizzle.config.ts
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 を使う必要がある。

こういうスクリプトを用意する。

db/migrate.ts
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 から実行

run.ts
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を作れるので、テストを簡単に作れる。

run.test.ts
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 にデプロイするフローを作る。

参考

https://deno.com/blog/build-database-app-drizzle
https://orm.drizzle.team/docs/migrations
https://github.com/drizzle-team/drizzle-orm/discussions/2532
https://github.com/davesteinberg/deno-drizzle-turso

Discussion