コンテンツにスキップ

Query Builder

Query builder は両エンジンで同一です。ここに書かれている全ては、 mutator の中 (ブラウザは IDB、Worker は D1)、useLiveQuery(...) の factory 内、createServerDb 経由の raw executor に対して、そのまま 動きます。

// 全カラム
const rows = await db.select().from(todos)
// 明示 projection
const rows = await db.select({ id: todos.id, title: todos.title }).from(todos)
// 集約付き projection
const stats = await db
.select({ userId: todos.userId, n: count() })
.from(todos)
.groupBy(todos.userId)

Row 形状は projection に従います。projection を書かなければ全カラム、 書けばその shape。

import { and, eq, gt, gte, inArray, isNull, isNotNull, like, lt, ne, not, or } from "@sh1n4ps/plasma-core"
// 比較
db.select().from(todos).where(eq(todos.done, 0))
db.select().from(todos).where(gt(todos.updatedAt, cutoff))
db.select().from(todos).where(ne(todos.userId, banned))
// 論理
db.select().from(todos).where(and(eq(todos.done, 0), gt(todos.updatedAt, cutoff)))
db.select().from(todos).where(or(eq(todos.priority, 1), eq(todos.pinned, 1)))
db.select().from(todos).where(not(eq(todos.done, 1)))
// 集合
db.select().from(todos).where(inArray(todos.userId, ["u1", "u2", "u3"]))
// Null チェック
db.select().from(todos).where(isNull(todos.assignee))
db.select().from(todos).where(isNotNull(todos.assignee))
// パターン
db.select().from(todos).where(like(todos.title, "% urgent %"))

and / or は 2 個以上の述語を取ります。ネストも可能: and(or(a, b), c)

import { asc, desc } from "@sh1n4ps/plasma-core"
db.select().from(todos).orderBy(asc(todos.updatedAt))
db.select().from(todos).orderBy(desc(todos.priority), asc(todos.updatedAt))

複数指定は安定ソート: 第 1 → 第 2 → …

// 先頭 20 件
db.select().from(todos).limit(20)
// 2 ページ目 (21〜40)
db.select().from(todos).limit(20).offset(20)
// ソート + ページング — orderBy と併用して deterministic に
db.select()
.from(todos)
.orderBy(desc(todos.updatedAt))
.limit(20)
.offset((page - 1) * 20)
db.select()
.from(todos)
.innerJoin(users, eq(todos.userId, users.id))
// Left join — users.* は nullable
db.select()
.from(todos)
.leftJoin(users, eq(todos.userId, users.id))

Join 後の row 形状:

{ todos: { id, title, ... }, users: { id, name, ... } | null }

複数 join:

db.select()
.from(todos)
.innerJoin(users, eq(todos.userId, users.id))
.leftJoin(comments, eq(comments.todoId, todos.id))
.where(eq(users.id, ctx.userId))
import { avg, count, max, min, sum } from "@sh1n4ps/plasma-core"
// ユーザーごとの todo 数
db.select({ userId: todos.userId, n: count() })
.from(todos)
.groupBy(todos.userId)
// priority ごとの average
db.select({ priority: todos.priority, avg: avg(todos.score) })
.from(todos)
.groupBy(todos.priority)

groupBy なしの集約は 1 行の結果になります。

where はグループ化前の行フィルタ、having は集約後のグループ フィルタです。

// todo が 3 件超のユーザーのみ
db.select({ userId: todos.userId, n: count() })
.from(todos)
.groupBy(todos.userId)
.having(gt(count(), 3))
// 組み合わせ — 最近完了した todo を、5 件以上あるユーザーだけ
db.select({ userId: todos.userId, n: count() })
.from(todos)
.where(and(eq(todos.done, 1), gt(todos.updatedAt, cutoff)))
.groupBy(todos.userId)
.having(gte(count(), 5))

fromSubquery + colRef — サブクエリを FROM に

Section titled “fromSubquery + colRef — サブクエリを FROM に”

サブクエリの結果を外側クエリの FROM 節として使います。「集約して から集約カラムで絞る」パターンで、単純な WHERE + HAVING では表せ ないケースに便利。

import { colRef, count, gt } from "@sh1n4ps/plasma-core"
// 内側: ユーザーごとの active todo 数
const active = db
.select({ userId: todos.userId, cnt: count() })
.from(todos)
.where(eq(todos.done, 0))
.groupBy(todos.userId)
// 外側: active > 3 のユーザーだけを join して名前も取る
const busy = await db
.select()
.fromSubquery(active, "active")
.innerJoin(users, eq(colRef("active", "userId"), users.id))
.where(gt(colRef("active", "cnt"), 3))

colRef(tableAlias, column) はサブクエリの projection から カラムを参照します。tableAlias.fromSubquery(inner, "alias") の第 2 引数と一致させます。

上記の全 builder 形状は .live()LiveQuery<T> に:

const live = db.select().from(todos).where(eq(todos.done, 0)).live()
live.subscribe((rows) => setRows(rows))

React の useLiveQuery factory として:

const rows = useLiveQuery(
() => plasma.db.select().from(todos).where(eq(todos.done, 0)),
[],
)

Reactivity の詳細は Live queries を 参照。

述語は値のように合成できます — 事前に組み立てて where に渡す:

function todoFilter(criteria: { done?: number; userId?: string }) {
const parts = []
if (criteria.done !== undefined) parts.push(eq(todos.done, criteria.done))
if (criteria.userId !== undefined) parts.push(eq(todos.userId, criteria.userId))
if (parts.length === 0) return undefined
return parts.length === 1 ? parts[0] : and(...parts)
}
const predicate = todoFilter({ done: 0, userId: currentUser })
const rows = await db.select().from(todos).where(predicate!)
  • Live queries — 上記 builder をリアク ティブ購読にする方法 (IVM 対応 / subscribeDelta / whenReady)
  • Mutators — 変更用の同じ db.insert / update / delete の書き方
  • Schema — このクエリの対象となる column DSL
  • Drizzle からの移行 — 全 operator の Drizzle 対応表