This builds on Drizzle Query Builder — an in-memory, chainable query over an array of plain-object rows that reads like a sentence and stays lazy until a terminal .all(). Here you add the step a real query almost always needs: projection. A UI table rarely wants every column a record carries; an API response often renames name to fullName or id to userId. So you'll add a terminal-shaping method .select(...) that picks a subset of columns, or renames them, and prove it composes cleanly with the filter / sort / paginate stages already in place. This question is self-contained — you reimplement the whole builder, including the operator helpers; no code is shared with the base question.
A predicate here is a function that takes one row and returns true or false. A projection is the shape you want each output row to have: either a list of columns to keep, or a map from output name to source column.
// query(rows) returns a chainable, lazy builder. Build steps accumulate;
// .all() runs filter → sort → offset → limit → project and returns a NEW array
// of NEW row objects.
query(rows: object[]): Builder
interface Builder {
where(cond: (row) => boolean): Builder; // chain multiple → AND
orderBy(key: string, dir?: 'asc' | 'desc'): Builder; // dir defaults to 'asc'
limit(n: number): Builder;
offset(n: number): Builder;
select(projection: string[] | Record<string, string>): Builder; // shape the output
all(): object[]; // terminal — runs the plan
}
// select() takes ONE of two projection shapes:
// ['id', 'name'] keep only these columns, in this order
// { userId: 'id', who: 'name' } rename: output key ← source column
// Operator helpers each return a predicate (row) => boolean:
eq(key, value) ne(key, value)
gt(key, value) gte(key, value)
lt(key, value) lte(key, value)
and(...conds) or(...conds) // combine predicates into one predicate
query is also exported under the alias drizzleQueryBuilderIi.
const users = [
{ id: 1, name: 'Ada', age: 36, role: 'admin' },
{ id: 2, name: 'Bo', age: 22, role: 'user' },
{ id: 3, name: 'Cy', age: 41, role: 'user' },
{ id: 4, name: 'Di', age: 22, role: 'admin' },
{ id: 5, name: 'Eve', age: 30, role: 'user' },
];
// Array projection: keep only the columns the UI needs.
query(users).where(eq('role', 'user')).select(['id', 'name']).all();
// → [{ id: 2, name: 'Bo' }, { id: 3, name: 'Cy' }, { id: 5, name: 'Eve' }]
// role=user → ids [2,3,5]; each row reshaped to just id + name.
// Object projection (alias) + the full pipeline. Note we filter on `role`
// and sort on `age`, then project them BOTH away in the output.
query(users)
.where(eq('role', 'user'))
.orderBy('age', 'desc')
.offset(1)
.limit(1)
.select({ id: 'id', label: 'name' })
.all();
// → [{ id: 5, label: 'Eve' }]
// role=user → [2,3,5]; age desc → [3,5,2]; offset 1 → [5,2]; limit 1 → [5];
// project → rename name→label, drop age and role.
.all() filters, sorts, offsets, limits, and only then projects. That ordering is what lets you .where(eq('role','user')) or .orderBy('age') on a column you don't .select() — those columns must still exist while filtering and sorting happen. Project first and you'd have dropped the very column you need to filter on.['id','name'] keeps exactly those columns (in that order). An object { out: 'src' } renames: each output key gets the value of its source column. You support one or the other per select, not a mix.{ a: 'name', b: 'name' } both a and b come from name — mapping two outputs to one source is allowed. Projecting a key the row doesn't have (['id','email'] when there's no email) yields undefined for that key, and the key is still present in the output object..all() returns new row objects shaped by the projection; writing to a returned row must not touch the source. Sorting also happens on a copy..select() means full rows. If you never call .select(), .all() returns rows unchanged in shape (every column).orderBy direction defaults to 'asc'; chained .where() calls AND-combine; offset is applied before limit. These carry over unchanged from the base builder.fullName from first + last), distinct, joins across tables (that's drizzle-query-builder-iii), or compiling to real SQL — those are listed under Going further in the solution.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.