Query Builder
The query builder is the same on both engines. Everything on this
page works identically inside a mutator (against IDB in the browser,
D1 on the Worker), inside a useLiveQuery(...) factory, and against
a raw executor via createServerDb.
select — column projection
Section titled “select — column projection”// All columnsconst rows = await db.select().from(todos)
// Explicit projectionconst rows = await db.select({ id: todos.id, title: todos.title }).from(todos)
// Computed projection with aggregatesconst stats = await db .select({ userId: todos.userId, n: count() }) .from(todos) .groupBy(todos.userId)The row shape flows through the projection. No projection = full row. Explicit projection = the shape you named.
where — filtering
Section titled “where — filtering”import { and, eq, gt, gte, inArray, isNull, isNotNull, like, lt, ne, not, or } from "@sh1n4ps/plasma-core"
// Comparisondb.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))
// Boolean logicdb.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)))
// Set membershipdb.select().from(todos).where(inArray(todos.userId, ["u1", "u2", "u3"]))
// Null checksdb.select().from(todos).where(isNull(todos.assignee))db.select().from(todos).where(isNotNull(todos.assignee))
// Patterndb.select().from(todos).where(like(todos.title, "% urgent %"))and / or accept 2+ predicates. Nesting is allowed:
and(or(a, b), c).
orderBy — sorting
Section titled “orderBy — sorting”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))Multiple orderings are stable: primary → secondary → …
limit and offset — pagination
Section titled “limit and offset — pagination”// First 20db.select().from(todos).limit(20)
// Page 2 (rows 21-40)db.select().from(todos).limit(20).offset(20)
// Sortable pagination — always pair with orderBy for deterministic pagesdb.select() .from(todos) .orderBy(desc(todos.updatedAt)) .limit(20) .offset((page - 1) * 20)innerJoin / leftJoin — joins
Section titled “innerJoin / leftJoin — joins”db.select() .from(todos) .innerJoin(users, eq(todos.userId, users.id))
// Left join — users.* is nullabledb.select() .from(todos) .leftJoin(users, eq(todos.userId, users.id))Joined row shape:
{ todos: { id, title, ... }, users: { id, name, ... } | null }Multiple joins:
db.select() .from(todos) .innerJoin(users, eq(todos.userId, users.id)) .leftJoin(comments, eq(comments.todoId, todos.id)) .where(eq(users.id, ctx.userId))groupBy + aggregates
Section titled “groupBy + aggregates”import { avg, count, max, min, sum } from "@sh1n4ps/plasma-core"
// How many todos per userdb.select({ userId: todos.userId, n: count() }) .from(todos) .groupBy(todos.userId)
// Average per prioritydb.select({ priority: todos.priority, avg: avg(todos.score) }) .from(todos) .groupBy(todos.priority)Aggregates without groupBy produce a single-row result.
having — post-aggregate filter
Section titled “having — post-aggregate filter”where filters rows before grouping. having filters groups
after aggregation.
// Only users with more than 3 todosdb.select({ userId: todos.userId, n: count() }) .from(todos) .groupBy(todos.userId) .having(gt(count(), 3))
// Combine — recent completed todos, only counting users with 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 — subqueries as sources
Section titled “fromSubquery + colRef — subqueries as sources”Use a subquery’s result as the FROM clause of an outer query. Useful for “compute aggregates, then filter by them” patterns where a single WHERE + HAVING wouldn’t work.
import { colRef, count, gt } from "@sh1n4ps/plasma-core"
// Inner: count of active todos per userconst active = db .select({ userId: todos.userId, cnt: count() }) .from(todos) .where(eq(todos.done, 0)) .groupBy(todos.userId)
// Outer: only users with > 3 active todos, then join their nameconst busy = await db .select() .fromSubquery(active, "active") .innerJoin(users, eq(colRef("active", "userId"), users.id)) .where(gt(colRef("active", "cnt"), 3))colRef(tableAlias, column) references a column from the
subquery’s projection. The alias ("active" above) matches the
second argument to .fromSubquery(...).
Live vs one-shot
Section titled “Live vs one-shot”Every builder shape above accepts .live() to become a
LiveQuery<T>:
const live = db.select().from(todos).where(eq(todos.done, 0)).live()live.subscribe((rows) => setRows(rows))Or as a useLiveQuery factory in React:
const rows = useLiveQuery( () => plasma.db.select().from(todos).where(eq(todos.done, 0)), [],)See Live queries for reactivity mechanics.
Building predicates dynamically
Section titled “Building predicates dynamically”Predicates compose like values — build them ahead of time and
pass them into 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!)What to read next
Section titled “What to read next”- Live queries — turning any of these builders into a reactive subscription (IVM eligibility, subscribeDelta, whenReady)
- Mutators — writing the same
db.insert / update / deleteshapes for changes - Schema — the column DSL these queries run against
- Migrating from Drizzle — every operator side-by-side with Drizzle