ModelQueryBuilder (Type-Safe)
The ModelQueryBuilder
is the primary, partially type-safe query API for Hysteria ORM models. All calls to User.query()
, Post.query()
, etc., return a ModelQueryBuilder instance.
The query builder is partially type safe, triggering intellisense for developer experience but still allowing the developer to write whatever he wants.
The ModelQueryBuilder by default returns a ModelWithoutRelations<T>
type, which is a type that does not have any relations. You can add Relations or Annotations using the load
and annotate
methods.
Key Features
- Fully partially type-safe queries and results
- Decorator and relation support
- Rich API for filtering, selecting, joining, and more
- Supports advanced features like CTEs, annotations, pagination, and soft deletes
Best Practice: Use ModelQueryBuilder for all application logic and model-based queries.
Example Usage
const users = await User.query().where('status', 'active').many();
Filtering
const users = await User.query().where('age', '>', 18).andWhere('isActive', true).many();
Raw right-hand side with rawStatement
When you need to compare a column to another column or an expression without creating a binding, use sql.rawStatement
. Identifiers inside the raw string are automatically quoted per database dialect.
import { sql } from 'hysteria-orm';
await User.query()
.where('id', sql.rawStatement('user.id'))
.many();
Selecting Columns
const names = await User.query().select('name').many();
Pagination
const page = await User.query().paginate(1, 10);
console.log(page.data, page.paginationMetadata);
Joins
const postsWithUsers = await Post.query()
.join('users', 'posts.userId', 'users.id')
.select('posts.*', 'users.name')
.many();
Annotations
const user = await User.query()
.annotate('COUNT(*)', 'count')
.first();
console.log(user.$annotations.count);
CTEs (Common Table Expressions)
const users = await User.query()
.with('users_cte', (qb) => qb.select('name').where('isActive', true))
.many();
Pluck
const names = await User.query().pluck('name'); // string[]
Increment / Decrement
await User.query().increment('age', 1);
await User.query().decrement('age', 1);
Locking
const users = await User.query().lockForUpdate().many();
const users = await User.query().forShare().many();
clearSelect
const users = await User.query().select('name').clearSelect().many();
Soft Delete
await User.query().softDelete({ column: 'deleted_at' });
Type Safety Example
// users: ModelWithoutRelations<User>
const users = await User.query().where('email', 'like', '%@example.com').many();
Comparison to QueryBuilder
- Type Safety: ModelQueryBuilder is fully typed, QueryBuilder is not.
- Decorator/Relation Support: Only ModelQueryBuilder supports model decorators and relations.
- Use Case: Use ModelQueryBuilder for all model logic; use QueryBuilder for raw SQL.
Full API Reference
Filtering
where
,orWhere
,andWhere
,whereIn
,whereNotIn
,whereNull
,whereNotNull
,whereBetween
,whereNot
,whereLike
,whereNotLike
,andWhereLike
,andWhereNotLike
,orWhereLike
,orWhereNotLike
,whereExists
,whereNotExists
await User.query().where('email', 'like', '%@example.com').many();
await User.query().whereIn('status', ['active', 'pending']).many();
await User.query().whereNull('deletedAt').many();
await User.query().whereBetween('age', [18, 30]).many();
Subqueries & Nested Conditions
whereSubQuery
,andWhereSubQuery
,orWhereSubQuery
,whereBuilder
,andWhereBuilder
,orWhereBuilder
await User.query().whereSubQuery('id', (sub) => sub.select('userId').from('posts').where('published', true)).many();
await User.query().whereBuilder((qb) => qb.where('age', '>', 18).orWhere('isActive', true)).many();
Joins
join
,leftJoin
,rightJoin
,innerJoin
(by table name or model)
await Post.query().join('users', 'posts.userId', 'users.id').many();
await Post.query().leftJoin(User, 'id', 'userId').many();
Group By & Having
groupBy
,having
await User.query().groupBy('status').having('COUNT(*)', '>', 1).many();
Unions
union
,unionAll
await User.query().select('name').union((qb) => qb.select('authorName')).many();
Aggregates
getCount
,getMax
,getMin
,getAvg
,getSum
const count = await User.query().getCount();
const maxAge = await User.query().getMax('age');
Annotations
annotate
,removeAnnotations
,clearRemoveAnnotations
await User.query().annotate('COUNT(*)', 'count').first();
await User.query().removeAnnotations().first();
Select & Raw Select
For models, only annotated columns are available and will be added to the $annotations
property of the model. Everything else will be ignored, if you need a query like selectRaw
you can use the QueryBuilder
instead.
select
,selectRaw
,clearSelect
await User.query().select('name', 'email').many();
await User.query().selectRaw('count(*) as count').first(); // will be ignored and `$additional` will be empty
await User.query().clearSelect().many();
Pluck
pluck
const names = await User.query().pluck('name');
Pagination
paginate
,limit
,offset
const page = await User.query().paginate(1, 10);
await User.query().limit(5).offset(10).many();
Locking
lockForUpdate
,forShare
await User.query().lockForUpdate().many();
await User.query().forShare().many();
CTEs (Common Table Expressions)
with
,withRecursive
,withMaterialized
await User.query().with('users_cte', (qb) => qb.select('name')).many();
Relation Loading (ModelQueryBuilder only)
load
,clearRelations
await User.query().load('posts').many();
await User.query().load('addresses', Address, (qb) => qb.load('users')).many();
Relation Existence Filters (havingRelated / notHavingRelated)
havingRelated
,orHavingRelated
,notHavingRelated
,orNotHavingRelated
Check whether a relation exists for the current model rows. You can pass either:
- a callback to add filters on the related query, or
- an operator/value pair to add a COUNT-based HAVING on the related rows.
By default, when no callback/operator/value is provided, the filter only checks that at least one related row exists.
// Base case: users that have at least one post
await User.query().havingRelated('posts').many();
// With a callback: users that have at least one published post
await User.query().havingRelated('posts', (qb) => {
qb.where('published', true);
}).many();
// With operator/value: users that have more than 5 posts
await User.query().havingRelated('posts', '>', 5).many();
// Negative variants
await User.query().notHavingRelated('posts').many(); // users with no posts
await User.query().orNotHavingRelated('posts', (qb) => qb.where('published', true)).many();
Supported relation types: hasOne
, belongsTo
, hasMany
, and manyToMany
.
Copying & Query Output
copy
,toQuery
,unWrap
const qb = User.query().where('isActive', true);
const qbCopy = qb.copy();
const sql = qb.toQuery();
ModelQueryBuilder Only
removeAnnotations
,clearRemoveAnnotations
,load
,clearRelations
Next: QueryBuilder (Raw SQL)