All questions

Drizzle Query Builder II

Premium

Drizzle Query Builder II

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.

Signature

// 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.

Examples

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.

Notes

  • Projection runs LAST. .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.
  • Two projection shapes, picked by type. An array ['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.
  • Alias semantics. With { 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.
  • The source is never mutated, and projected rows are fresh objects. .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.
  • No .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.
  • Don't worry about computed/derived columns (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.

Upgrade to Premium