Schema
schema は plasma アプリで最も重要なファイルです。このガイドでは、成長するプロジェクトに通常追加していく順序で、すべての宣言方法を解説します。
最小の table
Section titled “最小の table”import { defineSchema, id, table, text } from "@sh1n4ps/plasma-core"
const notes = table("notes", { id: id(), body: text(),})
export const schema = defineSchema({ notes })要件:
id()column はちょうど 1 つで、名前はidであること。 plasma の change log、IDB のkeyPath、blob-ref index はすべて、主キーがidという名前であることを前提にしています。id()column を別の名前にするとtable()実行時に throw します。defineSchemaのオブジェクトキーはtable(name, ...)の引数と一致すること。defineSchema({ notes: table("notes", ...) })は問題ありませんが、defineSchema({ n: table("notes", ...) })は明快なエラーメッセージとともに throw します。
Column type
Section titled “Column type”| ヘルパー | JavaScript 型 | SQL ストレージ |
|---|---|---|
id() |
string(デフォルトは UUID) |
TEXT PRIMARY KEY |
text() |
string |
TEXT |
int() |
number |
INTEGER |
bigint() |
bigint |
BIGINT |
boolean() |
boolean |
SQLite では 0/1、Postgres では BOOLEAN |
blob() |
Uint8Array |
BLOB(小さなインラインバイト列) |
json() |
unknown |
JSON 文字列 |
file() |
FileRef |
JSON 文字列({ hash, size, mime }) |
ref(() => other.id) |
参照先 column と同じ | TEXT 外部キー |
crdtCounter() |
number(sumCrdtCounter で読み取り) |
JSON { [clientID]: n } |
crdtPnCounter() |
number(pnRead で読み取り) |
JSON { p, n } |
crdtLwwRegister<T>() |
T(lwwRead で読み取り) |
JSON { value, ts, clientID } |
crdtOrSet<T>() |
T[](orSetValues で読み取り) |
JSON の tag-and-tombstone 構造 |
Modifier
Section titled “Modifier”すべての column は、modifier をチェインできる Column<...> を返します。modifier はすべて新しい Column を返す純粋関数です — 元の column は変更されないので、ベースを共有できます:
const nameCol = text()const users = table("users", { id: id(), firstName: nameCol, // TEXT NOT NULL middleName: nameCol.nullable(), // TEXT (nullable) handle: nameCol.unique(), // TEXT UNIQUE NOT NULL}).nullable()— column がnullを格納できるようになります。Selectの行はT | nullになり、Insert型ではこのフィールドがオプショナルになります(書き込み時にundefinedはnullとして実体化されるため、nullable な column からundefinedを読み取ることはありません)。.unique()— DB レベルの一意制約です。runMigrationsの DDL で適用されます。重複挿入は mutator 実行時に throw します。.default(value)— 挿入時に呼び出し側が column を省略したときに適用される値です。JSON シリアライズ可能な任意の値(またはcrypto.randomUUIDのような plasma ランタイムヘルパー)を指定できます。.encrypted()— クライアントエンジンが値を IDB に格納する前にEnvelopeでラップします。Encryption ガイド を参照してください。
id() — 主キー
Section titled “id() — 主キー”id() は text() 形状の column で、デフォルトが crypto.randomUUID() です。挿入時に id を渡すと plasma はそれを使い、省略すると randomUUID() を呼びます。
await db.insert(notes).values({ // id は UUID で自動補完される body: "hello",})
await db.insert(notes).values({ id: "custom-id", body: "hello",})id.default() は上書きできません — id()(text() に対して)の目的は、plasma のトリガーが依存できる安定した主キーであることを保証することにあります。
ref() — 外部キーとカスケード
Section titled “ref() — 外部キーとカスケード”const users = table("users", { id: id(), name: text() })
const todos = table("todos", { id: id(), title: text(), userId: ref(() => users.id, { onDelete: "cascade" }),})- ゲッター(
() => users.id)は遅延的に呼び出されるため、table を任意の順序で宣言できます。 - 参照先の column は別の
id()(またはtext())でなければなりません — plasma は複合キーをサポートしません。 onDelete:"noAction"(デフォルト) — 子行はダングリングな外部キーを持ったまま残ります。クライアントの optimistic ビューでは、サーバーが既に削除した親を一時的に表示することがあるため、plasma はこれを許容します。"cascade"— 親が削除されると子行も削除されます。"restrict"— 子行が存在する場合、親の削除は throw します。"setNull"— 親の削除時に外部キー column がnullに設定されます。子 column に.nullable()が必要です。
file() — バイナリ添付
Section titled “file() — バイナリ添付”const notes = table("notes", { id: id(), attachment: file().nullable(),})挿入時、呼び出し側は File、Blob、または既存の FileRef を渡せます:
await client.mutate("attach", { noteId: "n1", attachment: fileInput.files[0], // ブラウザの File オブジェクト})クライアントは outbox に入る前に生のアップローダーを FileRef にデシュガーし、バックグラウンドで PUT /sync/blob/:hash を通じて R2 にバイトをアップロードします。サーバー側では mutator は FileRef — { hash, size, mime } — を見るのであって、生のバイトを見ることはありません。
オプション:
file({ immutable: true, // デフォルト; 同じ行の ref を別のハッシュに再書き込みするのを禁止 maxSize: 25 * 1024 * 1024, // 25 MB を超えるアップロードを拒否 mimeAllowList: ["image/*", "application/pdf"],})完全な Files and blobs ガイドを参照してください。
CRDT column
Section titled “CRDT column”crdtCounter(grow-only)、crdtPnCounter(符号付き)、crdtLwwRegister<T>(register)、crdtOrSet<T>(observed-remove set)があります。それぞれに読み取りヘルパーと一連の mutation ヘルパーがあります:
const stats = table("stats", { id: id(), active: crdtPnCounter(), // 読み取り: pnRead(row.active)、書き込み: pnIncrement / pnDecrement})異なるタブからの並行書き込みは、サーバー上で自動的にマージされます(mergeCrdtColumns)。CRDT columns を参照してください。
Table オプション
Section titled “Table オプション”table() の第 3 引数は TableOptions オブジェクトを受け取ります:
const todos = table( "todos", { id: id(), title: text(), userId: ref(() => users.id) }, { auth: { read: (ctx, row) => row.userId === ctx.userId, write: (ctx, row) => row.userId === ctx.userId, }, resolveConflict: ({ server, client }) => ({ ...server, ...client, title: client.title, // クライアントの title を優先 done: Math.max(server.done ?? 0, client.done ?? 0), }), changeLogSuppressed: false, blobs: storageRef("primary"), },)auth— 行ごとの read/write 述語です。両エンジンで実行されます。Auth and permissions を参照してください。resolveConflict— 同じ行がローカルの optimistic 実行とサーバーによって並行更新されたときに呼ばれます。調整済みの行を返してください。Conflict resolution を参照してください。changeLogSuppressed— change_log トリガーをスキップします。ローカル専用の table(セッションドラフト、タブごとのキャッシュ)向けです。Offline mode を参照してください。blobs— この table のfile()column に対するストレージアダプターを上書きします。サーバーのSyncHandlerOptions.blobsで名前付きアダプターを宣言している必要があります。
defineSchema — table のグループ化
Section titled “defineSchema — table のグループ化”export const schema = defineSchema({ users, todos, comments,})
export const SCHEMA_VERSION = "todos-v1"- オブジェクトキーはすべてのエントリで
table(name, ...)の引数と一致しなければなりません。不一致は throw します。 SCHEMA_VERSIONは、追加的に非互換な変更を加えるたびにインクリメントする文字列です(Migrations を参照)。クライアントとサーバー間でバージョンが不一致だと 409 が発生し、onSchemaMismatchを通じてクライアントがリセットまたは継続する機会が得られます。
行の型を推論する
Section titled “行の型を推論する”import type { InferRow, InferInsertRow, InferUpdateRow } from "@sh1n4ps/plasma-core"
type Todo = InferRow<typeof todos>type TodoInsert = InferInsertRow<typeof todos>type TodoUpdate = InferUpdateRow<typeof todos>InferRow—db.select().from(...)が返すものInferInsertRow—db.insert().values(...)の呼び出し側の形状(デフォルトや.nullable()を持つ column はオプショナルになる)InferUpdateRow—db.update().set(...)の呼び出し側の形状(すべての column がオプショナルになる)
通常これらの型を明示的に書く必要はありません — クエリビルダーが推論してくれます。外部関数や React コンポーネントの prop に型を付ける必要があるときのためにあります。
リレーショナルクエリ — .query.x.findMany はどこ?
Section titled “リレーショナルクエリ — .query.x.findMany はどこ?”v1.0 では提供されていません。plasma のクエリビルダーは db.select().from(...).innerJoin(...).where(...) と leftJoin の同等物です。drizzle スタイルの .query.todos.findMany({ with: { comments: true } }) のようなトップレベルのリレーショナル API はまだありません。
回避策はクエリビルダーでの join です:
const rows = await db .select() .from(todos) .innerJoin(comments, eq(comments.todoId, todos.id)) .where(eq(todos.userId, ctx.userId))行の形状は、ネストされた { ...todo, comments: [] } ではなく、join されたタプル { todos: {...}, comments: {...} } になります。これが drizzle からの移行者にとって現時点での摩擦点です。ネストされたリレーショナル API は v1.1 の候補ですが、まだスコープが定まっていません。
次に読むべきもの
Section titled “次に読むべきもの”- Mutators — 定義したばかりの schema を操作する write 関数
- Migrations — 一度リリースした schema をどう進化させるか
- Live queries — schema をリアクティブに読み取る