Skip to content

Query Builder

The Query Builder provides a fluent, chainable API for building and executing queries on your models. It supports filtering, sorting, pagination, and data modification, all with a syntax inspired by SQL and modern ORMs.

Getting Started

typescript
// Fetch all users
const users = await User.query().fetch()

// Fetch users with conditions
const activeUsers = await User.query()
  .where('active', true)
  .fetch()

Filtering Records

where

Adds filter conditions to your query. Use this to select records matching specific criteria. Accepts key/value pairs, operators, objects, booleans, or callback functions for complex logic.

Parameter Table:

ParameterTypeDescription
callback(query) => voidGroup multiple conditions for advanced logic
conditionsRecord<string, unknown>Match all key-value pairs (AND logic)
keystringColumn name
valueanyValue to compare
operatorOperator (see Operators table below)Comparison or pattern operator
booleanbooleantrue: exists, false: does not exist (primary key)

Return: this (chainable)

Overloads:

  • where(callback: (query) => void): this — Group multiple conditions for advanced logic.
  • where(conditions: Record<string, unknown>): this — Match all key-value pairs (AND logic).
  • where<K extends string>(key: K, value: T[K]): this — Shorthand for equality.
  • where<K extends string>(key: K, operator: Operator, value: any): this — Use comparison or pattern operators.
  • where(value: boolean): thistrue for existence, false for non-existence of primary key.

Examples:

typescript
// By key/value
User.query().where('role', 'admin')

// By operator
User.query().where('age', '>=', 18)

// By object
User.query().where({ status: 'active', type: 'premium' })

// By boolean
User.query().where(true) // where primaryKey exists
User.query().where(false) // where primaryKey does not exist

// By callback (grouped conditions)
User.query().where(q => q.where('role', 'admin').orWhere('role', 'moderator'))

andWhere / orWhere

Adds additional AND/OR conditions to your query. Use andWhere to require more conditions, or orWhere to allow alternatives. All overloads from where are available.

Parameter Table:

ParameterTypeDescription
callback(query) => voidGroup multiple conditions for advanced logic
conditionsRecord<string, unknown>Match all key-value pairs (AND/OR logic)
keystringColumn name
valueanyValue to compare
operatorOperator (see Operators table below)Comparison or pattern operator
booleanbooleantrue: exists, false: does not exist (primary key)

Return: this (chainable)

Overloads:

  • andWhere(callback: (query) => void): this — Grouped AND logic.
  • andWhere(conditions: Record<string, unknown>): this — Add more AND key-value pairs.
  • andWhere<K extends string>(key: K, value: T[K]): this — AND equality.
  • andWhere<K extends string>(key: K, operator: Operator, value: any): this — AND with operator.
  • andWhere(value: boolean): this — AND with boolean existence.
  • orWhere(callback: (query) => void): this — Grouped OR logic.
  • orWhere(conditions: Record<string, unknown>): this — OR with key-value pairs.
  • orWhere<K extends string>(key: K, value: T[K]): this — OR equality.
  • orWhere<K extends string>(key: K, operator: Operator, value: any): this — OR with operator.
  • orWhere(value: boolean): this — OR with boolean existence.

Examples:

typescript
User.query().where('role', 'admin').andWhere('verified', true)
User.query().where('role', 'admin').orWhere('role', 'moderator')
User.query().andWhere(q => q.where('score', '>', 80).where('active', true))

Negation & Special Filters (All Variants)

Negation Methods

Negate any filter condition. Use these to exclude records matching certain criteria. All overloads from where/andWhere/orWhere are available as whereNot, andWhereNot, orWhereNot.

Parameter Table:

ParameterTypeDescription
callback(query) => voidGroup multiple conditions for advanced logic
conditionsRecord<string, unknown>Match all key-value pairs (AND/OR logic)
keystringColumn name
valueanyValue to compare
operatorOperator (see Operators table below)Comparison or pattern operator
booleanbooleantrue: exists, false: does not exist (primary key)

Return: this (chainable)

Examples:

typescript
User.query().whereNot('role', 'guest')
User.query().andWhereNot({ status: 'inactive' })
User.query().orWhereNot(q => q.where('score', '<', 50))

Special Filters

Fine-tune your queries with these helpers:

MethodParametersDescription
whereInkey, values: any[]Match if the column value is in the array
andWhereIn/orWhereInkey, values: any[]AND/OR variants
whereNotInkey, values: any[]Exclude values in the array
andWhereNotIn/orWhereNotInkey, values: any[]AND/OR variants
whereNullkeyMatch records where the column is null
andWhereNull/orWhereNullkeyAND/OR variants
whereNotNullkeyMatch records where the column is not null
andWhereNotNull/orWhereNotNullkeyAND/OR variants
whereBetweenkey, [min, max]Match values within a range (inclusive)
andWhereBetween/orWhereBetweenkey, [min, max]AND/OR variants
whereNotBetweenkey, [min, max]Exclude values within a range
andWhereNotBetween/orWhereNotBetweenkey, [min, max]AND/OR variants
whereLikekey, patternSQL LIKE pattern match (case-sensitive)
andWhereLike/orWhereLikekey, patternAND/OR variants
whereILikekey, patternCase-insensitive LIKE
andWhereILike/orWhereILikekey, patternAND/OR variants

Return: this (chainable)

Examples:

typescript
User.query().whereIn('role', ['admin', 'user'])
User.query().whereNotIn('status', ['inactive', 'banned'])
User.query().whereNull('deletedAt')
User.query().whereNotNull('email')
User.query().whereBetween('age', [18, 30])
User.query().whereNotBetween('score', [0, 100])
User.query().whereLike('name', 'J%')
User.query().whereILike('email', '%@gmail.com')

Operators

The following operators are available for use in where, andWhere, orWhere, and their negation variants. Use these to compare, match, or check for existence/null:

OperatorDescription
=, isEqual to
!=, is notNot equal to
<Less than
<=Less than or equal to
>Greater than
>=Greater than or equal to
likeSQL LIKE pattern match (case-sensitive)
not like, <>Not LIKE pattern (case-sensitive)
inValue is in array
not inValue is not in array
betweenValue is between two values (inclusive)
not betweenValue is NOT between two values
nullValue is null (use whereNull)
not nullValue is not null (use whereNotNull)
existsValue exists (not null/undefined)
not existsValue does not exist (null/undefined)
ilikeLIKE, case-insensitive
not ilikeNOT LIKE, case-insensitive

Chaining, Immutability, and TypeScript Tips

Chaining

All query builder methods return this, so you can chain calls to build up complex queries in a readable way.

Immutability & Side Effects

Query builders are mutable. If you reuse a builder, subsequent changes will affect all references. Use .clone() to create a new, independent builder if you want to branch your logic.

typescript
const base = User.query().where('active', true)
const admins = base.clone().where('role', 'admin').fetch() // Only active admins
const mods = base.clone().where('role', 'moderator').fetch() // Only active moderators

TypeScript Usage Tips

  • Type safety is provided by the model class itself. When you call User.query(), the resulting builder is already typed for the model's fields.
  • All methods are strongly typed; invalid keys/operators will be caught by TypeScript.
  • Use type inference for model fields in conditions.
  • If you need to use dynamic keys, cast as keyof T or use type assertions.
  • For advanced use, you can explicitly type the builder:
typescript
const q: ReactiveQueryBuilder<User> = User.query().where('active', true)
  • All filter methods return this for chaining.

Handling Result Types from fetch and then

The result of fetch (or using .then on the builder) depends on your query:

  • fetch() returns a Promise<Array<User>> (or your model type) for multi-record queries.
  • first(), last(), or similar methods return a Promise<User | undefined>.
  • If you use .then directly on the builder, the resolved value matches the terminal method (e.g., fetch, first).

TypeScript Example:

typescript
// Multiple results
const users: User[] = await User.query().where('active', true).fetch()

// Single result (may be undefined)
const firstUser: User | undefined = await User.query().where('active', true).first()

// Using .then
User.query().where('active', true).fetch().then((users) => {
  // users: User[]
})
User.query().where('active', true).first().then((user) => {
  // user: User | undefined
})
  • Always check for undefined when using first, last, or similar methods.
  • Use explicit type annotations if you need to clarify the result type for downstream code or editors.

Query Builder Utilities

clear

Removes all conditions and clauses from the builder. Use to reset a query and start over.

typescript
const query = User.query().where('role', 'admin')
query.clear()
ParameterTypeDescription
(none)
ReturnsTypeDescription
thisReactiveQueryBuilderThe same builder instance, cleared

Error Handling:

  • No errors are thrown. Safe to call at any time.

clone

Creates a new builder with the same conditions. Use to branch queries without side effects.

typescript
const base = User.query().where('active', true)
const admins = base.clone().where('role', 'admin').fetch()
const mods = base.clone().where('role', 'moderator').fetch()
ParameterTypeDescription
(none)
ReturnsTypeDescription
builderReactiveQueryBuilderA new builder instance with copied state

Error Handling:

  • No errors are thrown. The clone is always safe to use.

Error Handling & Pitfalls

  • Query results are not reactive unless you call .reactive(). By default, they are static snapshots.
  • Most filtering is in-memory. Large datasets and complex queries may impact performance.
  • Mutations (update/delete) fetch records first.
  • Query builders are mutable. Use clone() to avoid side effects.
Error/PitfallDescriptionHow to Avoid
Non-reactive resultsQuery results do not update automatically if data changesRe-run the query to get fresh data
In-memory filteringAll filtering is done in JavaScript, not in a databaseUse pagination and limit result set size
Mutations fetch firstupdate/delete always fetch records before mutatingBe aware of performance on large sets
Builder mutabilityQuery builder is mutable by defaultUse clone() for branching

API Reference (Summary)

MethodDescriptionChainableImmutability
where, andWhere, orWhere, whereNot, ...Filtering methodsYesNo (mutates builder)
orderBy, limit, offset, forPageSorting & paginationYesNo
update, delete, fetch, first, last, countData modification & retrievalNo (terminates chain)N/A
clear, cloneUtilitiesYesclone is immutable, clear mutates

See the full API documentation for detailed method signatures and type information.

TypeScript & Chaining Tips

  • All builder methods are chainable unless they return results (fetch, first, etc.).
  • Use clone() to branch queries safely in TypeScript.
  • The builder is generic: ReactiveQueryBuilder<T>. Type inference works with model classes.
  • Use explicit types for advanced patterns:
typescript
const q: ReactiveQueryBuilder<User> = User.query().where('active', true)
  • All filter methods return this for chaining.

Advanced Query Patterns

  • Branching:
typescript
const base = User.query().where('active', true)
const admins = base.clone().where('role', 'admin').fetch()
const mods = base.clone().where('role', 'moderator').fetch()
  • Grouped conditions:
typescript
User.query().where(q => q.where('role', 'admin').orWhere('role', 'moderator'))
  • Negation:
typescript
User.query().whereNot('status', 'inactive')
  • Pagination:
typescript
User.query().orderBy('created_at').forPage(2, 10)

Reactive Queries

Reactive queries provide live, event-driven updates to your query results. Instead of returning a static snapshot, a reactive query emits events when the underlying data changes (e.g., insert, delete, or any change that affects the result set). This is ideal for building real-time UIs or keeping data in sync automatically.

Usage

Call .reactive() on any query builder to get a reactive response factory. You can then call .fetch(), .first(), .last(), .forPage(), or .count() directly on the factory. The result is a reactive response object that emits events as the result set changes.

Example:

typescript
// Get a reactive collection of users
const collection = await User.query().where('active', true).reactive().fetch()
collection.value // Users[]
collection.on('next', users => {
  // users: User[]
  // Called initially and whenever the result set changes (insert/delete)
})
collection.on('error', err => { /* handle error */ })
collection.on('complete', () => { /* cleanup */ })

// Get a reactive single result
const firstUser = await User.query().where('active', true).reactive().first()
firstUser.value // User | undefined
firstUser.on('next', user => {
  // user: User | undefined
  // Called initially and whenever the first matching user changes (insert/delete)
})

// Unmount when done to clean up listeners
collection.unmount()
firstUser.unmount()

API

  • .reactive().fetch() — Returns a ReactiveQueryCollection (emits next with an array of models)
  • .reactive().first() — Returns a ReactiveQueryResult (emits next with a single model or undefined)
  • .reactive().last() — Returns a ReactiveQueryResult (emits next with a single model or undefined)
  • .reactive().forPage(page, perPage) — Returns a ReactiveQueryCollection for a specific page
  • .reactive().count() — Returns a ReactiveQueryResult (emits next with a count)

Notes

Reactivity is event-driven

The next event is only emitted when the result set changes (e.g., a record is inserted or deleted, or a filter causes the set to change). Updates to the properties of a model instance do not trigger next (the model instance itself is reactive).

Unmounting

Always call .unmount() on the response when you no longer need updates to avoid memory leaks.

Type safety

The reactive response is strongly typed to match your model and query.


API Consistency Table

MethodAND VariantOR VariantNegationIn/NotInNull/NotNullBetween/NotBetweenLike/ILike
whereandWhereorWherewhereNotwhereIn/whereNotInwhereNull/whereNotNullwhereBetween/whereNotBetweenwhereLike/whereILike
andWhereNotorWhereNotandWhereIn/andWhereNotInandWhereNull/andWhereNotNullandWhereBetween/andWhereNotBetweenandWhereLike/andWhereILike
orWhereIn/orWhereNotInorWhereNull/orWhereNotNullorWhereBetween/orWhereNotBetweenorWhereLike/orWhereILike

All variants support the same overloads and chaining. Negation and special filters are available for all logical operators.