Query Builder API
Fluent API for building complex database queries
select()
select(...fields: string[]): QueryBuilder<T>Specifies columns to select. Defaults to *.
await repo.find().select("id", "name", "email").execute(orm.client);where()
where(condition: string, ...params: any[]): QueryBuilder<T>Adds a WHERE clause. Multiple calls join with AND.
await repo.find().where("age > ?", 18).where("isActive = ?", true).execute(orm.client);orWhere()
orWhere(condition: string, ...params: any[]): QueryBuilder<T>Adds an OR WHERE clause.
await repo.find().where("role = ?", "admin").orWhere("role = ?", "moderator").execute(orm.client);whereIn() / whereNotIn()
whereIn(column: string, values: any[]): QueryBuilder<T>whereNotIn(column: string, values: any[]): QueryBuilder<T>await repo.find().whereIn("status", ["active", "pending"]).execute(orm.client);whereNull() / whereNotNull()
whereNull(column: string): QueryBuilder<T>whereNotNull(column: string): QueryBuilder<T>await repo.find().whereNotNull("email").execute(orm.client);whereBetween() / whereLike()
whereBetween(column: string, start: any, end: any): QueryBuilder<T>whereNotBetween(column: string, start: any, end: any): QueryBuilder<T>whereLike(column: string, pattern: string): QueryBuilder<T>whereILike(column: string, pattern: string): QueryBuilder<T>await repo.find().whereBetween("age", 18, 65).execute(orm.client);await repo.find().whereLike("email", "%@example.com").execute(orm.client);orderBy()
orderBy(column: string, direction: "ASC" | "DESC" = "ASC"): QueryBuilder<T>await repo.find().orderBy("createdAt", "DESC").execute(orm.client);orderByRaw() / groupByRaw() / havingRaw()
orderByRaw(expression: string, direction?: "ASC" | "DESC"): QueryBuilder<T>groupByRaw(expression: string): QueryBuilder<T>havingRaw(condition: string, ...params: any[]): QueryBuilder<T>For expressions rather than column names. Raw and plain clauses compose, and raw parameters are bound in the order they appear.
await repo.find() .select("status", "COUNT(*) AS total") .groupByRaw("strftime('%Y-%m', createdAt)") .havingRaw("COUNT(*) > ?", 10) .orderByRaw("CASE WHEN status = 'urgent' THEN 0 ELSE 1 END") .execute(orm.client);withRelations()
withRelations(...relations: (string | string[])[]): QueryBuilder<T>getRelations(): string[]Eager-loads relations onto the result, as findOne(id, { relations }) does. Nested paths use dot notation, and the loaded rows are cached with their relations.
const users = await repo.find() .where("isActive = ?", true) .withRelations("roles", "roles.permissions") .limit(10) .execute(orm.client);join() / innerJoin() / leftJoin()
join(table: string, condition: string): QueryBuilder<T>innerJoin(table: string, condition: string): QueryBuilder<T>leftJoin(table: string, condition: string): QueryBuilder<T>await repo.find().join("users", "posts.authorId = users.id").execute(orm.client);groupBy() / having()
groupBy(...columns: string[]): QueryBuilder<T>having(condition: string, ...params: any[]): QueryBuilder<T>await repo.find() .select("categoryId", "COUNT(*) as cnt") .groupBy("categoryId") .having("COUNT(*) > ?", 5) .execute(orm.client);limit() / offset() / paginate()
limit(limit: number): QueryBuilder<T>offset(offset: number): QueryBuilder<T>take(count: number): QueryBuilder<T> // alias of limitskip(count: number): QueryBuilder<T> // alias of offsetfirst(): QueryBuilder<T> // limit(1), returns the builderpaginate(page: number, pageSize: number): QueryBuilder<T>await repo.find().limit(10).offset(20).execute(orm.client);await repo.find().paginate(1, 20).execute(orm.client);scope()
scope(name: string, ...args: any[]): QueryBuilder<T>await repo.find().scope("active").scope("byRole", "admin").execute(orm.client);Raw & Existence
whereRaw(rawSql: string, ...params: any[]): QueryBuilder<T>whereRef(leftCol: string, op: string, rightCol: string): QueryBuilder<T>whereNot(condition: string, ...params: any[]): QueryBuilder<T>whereExists(builderOrSql: string | QueryBuilder<any>): QueryBuilder<T>whereNotExists(builderOrSql: string | QueryBuilder<any>): QueryBuilder<T>whereRef() compares two columns instead of a column and a bound value. whereExists() accepts a nested builder, which is rendered for the dialect the builder was given.
await repo.find() .whereRef("posts.authorId", "=", "users.id") .execute(orm.client);Aggregates & Select
select(...fields: string[]): QueryBuilder<T>selectRaw(expression: string, ...params: any[]): QueryBuilder<T>distinct(): QueryBuilder<T>count(column: string = "*", alias: string = "count"): QueryBuilder<T>sum(column: string, alias: string = "sum"): QueryBuilder<T>avg(column: string, alias: string = "avg"): QueryBuilder<T>min(column: string, alias: string = "min"): QueryBuilder<T>max(column: string, alias: string = "max"): QueryBuilder<T>The aggregate helpers add a column to the SELECT list; they do not execute anything. Read the value off the returned row.
const [row] = await repo.find().count("id", "total").execute(orm.client);// row.total
await repo.find().selectRaw("COALESCE(SUM(amount), 0) AS revenue").execute(orm.client);Joins, Locking & Set Operations
fullJoin(table: string, condition: string): QueryBuilder<T>crossJoin(table: string): QueryBuilder<T>
lock(mode: LockMode = "FOR UPDATE"): QueryBuilder<T>forUpdate(): QueryBuilder<T>forShare(): QueryBuilder<T>
union(builder: QueryBuilder<any>): QueryBuilder<T>unionAll(builder: QueryBuilder<any>): QueryBuilder<T>
with(name: string, builder: QueryBuilder<any>): QueryBuilder<T>withRecursive(name: string, builder: QueryBuilder<any>): QueryBuilder<T>lock() is emitted only for Postgres and MySQL; the repository's lockForUpdate() skips it elsewhere. with() and withRecursive() build CTEs.
const sub = new QueryBuilder("posts").select("authorId").where("published = ?", true);
await repo.find().union(sub).execute(orm.client);await repo.find().forShare().execute(orm.client);Dialect, Aliasing & Cloning
withDialect(dialect: DBType): QueryBuilder<T>as(alias: string): QueryBuilder<T>clone(): QueryBuilder<T>build(dialect?: DBType): { query: string; params: any[] }toSQL(dialect?: DBType): { query: string; params: any[] }clone() copies the builder so it can be reused —each() and eachBatch() rely on this to page a query without mutating it. toSQL() is an alias of build().
const base = repo.find().where("isActive = ?", true);
const { query, params } = base.clone().orderBy("name").toSQL(DBType.Postgres);build() / execute()
build(dialect?: DBType): { query: string; params: any[] }async execute(client: DBClient, cache?: Cache, cacheKey?: string): Promise<T[]>async countExec(client: DBClient): Promise<number>async existsExec(client: DBClient): Promise<boolean>execute() stamps the dialect from the client. It applies the row transform before writing to the cache and loads relations before the cache write, so a cache hit returns the same shape as a fresh read. Reads taken inside a transaction are never cached. countExec() and existsExec() are what the repository's count() and exists() call.
const { query, params } = repo.find().where("id = ?", 1).build();const results = await repo.find().where("id = ?", 1).execute(orm.client);