Drizzle からの移行
plasma の schema DSL と query builder は意図的に Drizzle 風に作られています。 Drizzle を知っていれば、plasma の read 側はほぼそのまま書けます。この guide では、既存の Drizzle プロジェクトを plasma に移すときに何が変わり、何が 変わらないかを整理します。
| レイヤー | Drizzle | plasma | 移行コスト |
|---|---|---|---|
| Schema DSL | pgTable("todos", { id: text().primaryKey() }) |
table("todos", { id: id() }) |
低 — helper 名が違うので sed 一発は無理 |
| Query builder | db.select().from().where(eq()) |
同じ形 | ほぼゼロ |
| Row 型推論 | $inferSelect / $inferInsert |
InferRow / InferInsertRow |
名前を書き換えるだけ |
| Write path | db.insert() をどこでも書く |
defineMutators に登録した mutator 経由のみ |
ここが再設計 |
| Migration | drizzle-kit (バージョン付き SQL file) | ensureSchema + runMigrations (runtime reconcile、加算のみ) |
メンタルモデルが違う |
| リレーション | db.query.todos.findMany({ with }) |
Builder の .innerJoin() / .leftJoin() |
呼び出し箇所ごとに書き換え |
| リアクティビティ | なし (Drizzle は一発クエリ ORM) | useLiveQuery によるリアクティブ購読 |
純増分 |
| Offline / sync | 自前実装が必要 | ビルトイン | 純増分 |
最も影響が大きい 2 行は、write path が mutator 中心になる ことと、 リレーションのシュガーが失われる ことです。それ以外は短い機械的な 書き換えで済みます。
そのまま使えるもの
Section titled “そのまま使えるもの”db.select().from().where().orderBy().limit()
Section titled “db.select().from().where().orderBy().limit()”// Drizzleconst rows = await db .select() .from(todos) .where(eq(todos.done, 0)) .orderBy(asc(todos.updatedAt)) .limit(20)
// plasma — まったく同じconst rows = await db .select() .from(todos) .where(eq(todos.done, 0)) .orderBy(asc(todos.updatedAt)) .limit(20)eq,ne,gt,gte,lt,lte,and,or,inArray,isNull,isNotNull,like,asc,desc,count,sum,avg,max,min— 全て@sh1n4ps/plasma-coreから Drizzle と同じ形で export されて います。db.insert().values(),db.update().set().where(),db.delete().where()も同じ形ですが、mutator の中でしか使えません (後述)。
innerJoin / leftJoin
Section titled “innerJoin / leftJoin”// Drizzleconst rows = await db .select() .from(todos) .innerJoin(users, eq(todos.userId, users.id)) .where(eq(users.id, ctx.userId))
// plasma — まったく同じconst rows = await db .select() .from(todos) .innerJoin(users, eq(todos.userId, users.id)) .where(eq(users.id, ctx.userId))Join 後の row は { todos: {...}, users: {...} } の形。projection を
書いていなければ Drizzle と同じ shape が返ります。
機械的に変わるもの
Section titled “機械的に変わるもの”Schema の宣言
Section titled “Schema の宣言”// Drizzle (Postgres)import { pgTable, text, integer, timestamp } from "drizzle-orm/pg-core"
export const todos = pgTable("todos", { id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), title: text("title").notNull(), done: integer("done").default(0).notNull(), userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), updatedAt: timestamp("updated_at").notNull(),})
// plasmaimport { defineSchema, id, int, ref, table, text } from "@sh1n4ps/plasma-core"
export const todos = table("todos", { id: id(), title: text(), done: int().default(0), userId: ref(() => users.id, { onDelete: "cascade" }), updatedAt: int(),})
export const schema = defineSchema({ users, todos })ポイント:
- Dialect フリー: plasma の
table()は SQLite (D1) でも Postgres でも同じ書き方です。Drizzle は宣言時にpgTable/sqliteTable/mysqlTableを選ぶ必要があります。 id()は必須で、名前もid固定: 複合 PK / serial 整数 PK は 非対応。change log と IDB key path がこれを前提にしています。timestampヘルパーは無し: epoch ms としてint()を使います。 v1.0 では Date-backed の column 型を出荷していません — timestamp は全てintで通します。$defaultFnは.default()にマップ — plasma でも default は 同じように適用されますが、値または serializable な関数を渡します。$onUpdateはなし —updatedAtは行に触れる全 mutator で 明示的にセットします。
Row 型の推論
Section titled “Row 型の推論”// Drizzletype Todo = typeof todos.$inferSelecttype NewTodo = typeof todos.$inferInsert
// plasmaimport type { InferRow, InferInsertRow, InferUpdateRow } from "@sh1n4ps/plasma-core"
type Todo = InferRow<typeof todos>type NewTodo = InferInsertRow<typeof todos>type TodoUpdate = InferUpdateRow<typeof todos>widening のルールは同じです: default 付きの column は
InferInsertRow で optional に、InferUpdateRow では全 column が
optional になります。
再設計が必要な箇所 — write path
Section titled “再設計が必要な箇所 — write path”構造的な変更として最も大きい部分です。Drizzle では
db.insert(todos).values(...) を コードのどこでも 書けます — route
handler、React component、scheduled job など。一方 plasma では、全ての
書き込みは事前に登録された mutator の中でしか行えません。
Before (Drizzle)
Section titled “Before (Drizzle)”// アプリ内のどこでもapp.post("/todos", async (req, res) => { const { title } = req.body await db.insert(todos).values({ id: crypto.randomUUID(), title, userId: req.user.id, updatedAt: new Date(), }) res.json({ ok: true })})After (plasma)
Section titled “After (plasma)”// src/shared/schema.ts — 一度だけ宣言するimport { defineMutators, eq } from "@sh1n4ps/plasma-core"
interface Ctx { userId: string }
export const mutators = defineMutators<typeof schema, Ctx>()({ createTodo: async ({ db, args, ctx }) => { await db.insert(todos).values({ id: args.id, title: args.title, userId: ctx.userId, updatedAt: args.updatedAt, }) },})todo を作りたい任意の場所から:
// React からconst create = useMutation<typeof mutators, "createTodo">("createTodo")create.mutate({ id: plasma.newId(), title: "牛乳を買う", updatedAt: Date.now(),})
// プレーンな Node スクリプトからawait plasma.mutate("createTodo", { id: ..., title: ..., updatedAt: ... })なぜこの変更が必要か
Section titled “なぜこの変更が必要か”mutator の中の db は engine ごとに違う実装がバインドされます。同じ
関数、2 つの runtime: ブラウザでは IDB に対して optimistic に (だから
mutate() は即座に返る)、Worker では push を受けてから D1 に対して
canonical に。mutator の外から db.insert() を呼べてしまうと、optimistic
側の実行に対応する経路がありません — だから plasma は write を
registry に集約しています。
機械的なポート手順
Section titled “機械的なポート手順”コードベース内の Drizzle db.insert/update/delete 呼び出し箇所ごとに:
- 引数を抽出する (
.values()/.set()に渡している値と where 句)。 - アクション名を付けて mutator でラップする。
defineMutators({...})に登録する。- 呼び出し箇所を
client.mutate("actionName", args)(ブラウザ) またはplasma.mutate("actionName", args)(Node / テスト) に置き換える。
シュガーが失われる部分 — リレーション
Section titled “シュガーが失われる部分 — リレーション”Drizzle のトップレベルなリレーショナル API:
// Drizzleconst withComments = await db.query.todos.findMany({ with: { comments: true }, where: eq(todos.userId, ctx.userId),})// 返り値: [{ id, title, comments: [{...}, ...] }, ...]v1.0 の plasma には db.query.*.findMany({ with }) はありません。通常の
join で書き換えます:
// plasma — flat な join tupleconst rows = await db .select() .from(todos) .leftJoin(comments, eq(comments.todoId, todos.id)) .where(eq(todos.userId, ctx.userId))// 返り値: [{ todos: {...}, comments: {...} | null }, ...]nest した構造が欲しければ呼び出し側で手動で group してください。 nested-relational のシュガーは v1.1 のキュー にあります。 Drizzle 移行者にとっては最大の papercut です。
plasma で得られるもの
Section titled “plasma で得られるもの”Drizzle 単体には無い機能 — 移行を選ぶ理由はここにあります。
リアクティブなライブクエリ
Section titled “リアクティブなライブクエリ”function TodoList() { const rows = useLiveQuery( () => plasma.db.select().from(todos).where(eq(todos.done, 0)), [], ) return <ul>{rows.map((t) => <li key={t.id}>{t.title}</li>)}</ul>}対象テーブルに変更があれば、それが別タブや別ユーザーからの sync 経由でも、component が再描画されます。裏では IVM engine が変更のあった row だけを diff — 10k row のテーブルに 1 件 insert しても、top-20 window は再描画されません。
オフライン outbox + optimistic UI
Section titled “オフライン outbox + optimistic UI”create.mutate(...) は IDB から即座に return します。mutation は
network で確定するまで outbox に貯まります。タブを閉じて明日開いても、
まだ確定していない writes が flush されます。Worker を落としても
mutation は動き続けます。この機能に対して追加コードはゼロです。
CRDT カラム
Section titled “CRDT カラム”衝突なしで収束させたい state 用:
const messages = table("messages", { id: id(), reactions: crdtOrSet<string>(), // observed-remove set})
// 同じ emoji の同時 add + remove は「add wins」で解決。Drizzle では merge ロジックを自作しないと実現できません。詳細は CRDT ガイド。
ファイル添付 (R2)
Section titled “ファイル添付 (R2)”file() column + usePlasmaFile(ref) hook。content-addressable、
edge cache 付き、auth 制御込み。Drizzle-with-R2 だと upload 周りを
手で組む必要があります。
plasma が向かないケース
Section titled “plasma が向かないケース”Drizzle プロジェクトが以下のどれかに当てはまるなら、plasma への 移行はおそらく合いません:
- Public feed — Twitter timeline 型、1 行を数千 subscriber に ファンアウトする用途。plasma の change log は書き込みで肥大化する ため、per-user データには合いますが broadcast には向きません。
- Analytics / batch aggregation — plasma の IVM は window 単位。 columnar OLAP と競合できません。
- 複合主キー — plasma は string の
idを単一の PK とすることを 要求します。 - サーバー専用の multi-tenant — ブラウザ側が無いなら sync layer の恩恵はありません。
移行チェックリスト
Section titled “移行チェックリスト”中規模の Drizzle プロジェクト (10-30 tables、50-100 mutation site) なら:
-
pgTable/sqliteTable→tableに置換 (dialect フリー)。 1 table 1 file の構成は維持できます。 - schema の末尾に
defineSchema({...})を追加。 - timestamp column を全て
int()(epoch ms) に。 - UUID text の PK を使っていなかった table に
id: id()を追加。 -
$inferSelect→InferRow、$inferInsert→InferInsertRowに書き換え。 -
db.insert/update/deleteの全呼び出しを名前付き mutator に 集約し、defineMutatorsに登録。 - row 単位アクセスは
TableOptions.authに per-table 述語を宣言。 多くの middleware がここに移行できます。 - 呼び出し箇所を
client.mutate("name", args)/useMutationに置換。 -
.query.x.findMany({ with })を.innerJoin()/.leftJoin()に書き換え。 - drizzle-kit の migration 呼び出しを deploy 時 (or admin route)
の
runMigrationsに差し替え。 - plasma 導入前クライアントと互換性のない最初の deploy で
SCHEMA_VERSIONを bump。
Drizzle 移行者が触りそうな規模のアプリなら、port 作業は数日の 機械的な作業です。Drizzle → plasma のバリュースワップは、大抵の チームがそれをやる価値ありと判断できる規模になっています。
次に読むページ
Section titled “次に読むページ”- Schema — 全 column 型と modifier
- Mutators — mutator の契約詳細
- Live queries — リアクティブ read path
- Migrations —
ensureSchema/runMigrations/SCHEMA_VERSION - Todo App レシピ — 動くプロジェクト