Query Builder
- Introduction
- Running Queries
- Select
- Where Clauses
- Ordering, Grouping, Limit
- Joins
- Aggregates
- Raw Expressions
- Insert, Update, Delete
- Subqueries in Select
- Transactions
Introduction
Every Model has a fluent query builder accessible via Model.query() or any static query method (where, orderBy, etc.). You can also use the QueryBuilder directly against a connection for raw query building.
The query builder produces dialect-correct SQL for all five supported databases. You never write a parameter placeholder yourself — Orion handles it per driver ($1/$2 for Postgres, ? for MySQL/SQLite/MariaDB, @p1 for SQL Server).
Running Queries
// get() returns ModelCollection<T>
const users = await User.where('is_active', true).get();
// first() returns T | null
const user = await User.where('email', 'alice@example.com').first();
// find() by PK
const user = await User.find(1);
// Pluck a single column as an array
const emails = await User.where('is_active', true).pluck('email');
// ['alice@example.com', 'bob@example.com']
// Value — single column from first row
const name = await User.where('id', 1).value('name');Select
// Select specific columns
const users = await User.select('id', 'name', 'email').get();
// Add columns on top of an existing select
const users = await User.select('id').addSelect('name').get();
// Distinct
const countries = await User.distinct().pluck('country');Where Clauses
Basic Where
The where method accepts three forms:
// Column, value (implicit =)
User.where('is_active', true)
// Column, operator, value
User.where('age', '>=', 18)
User.where('name', '!=', 'Guest')
User.where('score', '<>', 0)
// Supported operators: = != <> < > <= >= LIKE NOT LIKE ILIKEorWhere
const users = await User
.where('role', 'admin')
.orWhere('role', 'editor')
.get();Note:
orWhereat the top level can produce unintended results when combined with otherwherecalls. Use logical grouping for complex conditions.
whereIn / whereNotIn
await User.whereIn('id', [1, 2, 3]).get();
await User.whereNotIn('status', ['banned', 'suspended']).get();
await User.orWhereIn('role', ['admin', 'moderator']).get();whereNull / whereNotNull
await User.whereNull('deleted_at').get();
await User.whereNotNull('email_verified_at').get();
await User.orWhereNull('bio').get();whereBetween
await Order.whereBetween('total', [100, 500]).get();
await Order.whereNotBetween('created_at', [startDate, endDate]).get();whereLike
await User.whereLike('name', 'Ali%').get(); // case-sensitive
await User.whereILike('name', 'ali%').get(); // case-insensitive
await User.whereNotLike('email', '%@spam.com').get();whereDate / whereYear / whereMonth
await Order.whereDate('created_at', '2024-01-15').get();
await Order.whereYear('created_at', 2024).get();
await Order.whereMonth('created_at', 1).get(); // January
await Order.whereDay('created_at', 15).get();
await Order.whereTime('created_at', '>', '09:00').get();whereColumn
Compare two columns in the same row:
await Order.whereColumn('shipped_at', '>', 'created_at').get();
await Order.whereColumn('first_name', 'last_name').get(); // implicit =Logical Grouping
Use a closure to group OR conditions so they don't bleed into other clauses:
const users = await User
.where('is_active', true)
.where((q) => {
q.where('role', 'admin').orWhere('is_superuser', true);
})
.get();
// SELECT * FROM users WHERE is_active = true AND (role = 'admin' OR is_superuser = true)Subquery Where
// whereExists
const users = await User
.whereExists((q) => {
q.from('orders').whereColumn('orders.user_id', 'users.id');
})
.get();
// whereNotExists
const users = await User.whereNotExists((q) => {
q.from('orders').whereColumn('orders.user_id', 'users.id');
}).get();
// Subquery value comparison
const users = await User.where('balance', '>', (q) => {
q.from('accounts').selectRaw('AVG(balance)').whereColumn('user_id', 'users.id');
}).get();Ordering, Grouping, Limit
orderBy
await User.orderBy('name').get(); // ASC by default
await User.orderBy('created_at', 'desc').get();
await User.orderBy('last_name').orderBy('first_name').get(); // chain
await User.latest().get(); // ORDER BY created_at DESC
await User.oldest().get(); // ORDER BY created_at ASC
await User.inRandomOrder().get();groupBy / having
const stats = await Order
.select('user_id')
.selectRaw('COUNT(*) as order_count')
.selectRaw('SUM(total) as total_amount')
.groupBy('user_id')
.having('order_count', '>', 5)
.get();limit / offset / forPage
await User.limit(10).get();
await User.limit(10).offset(20).get();
await User.forPage(3, 15).get(); // page 3 with 15 per page = LIMIT 15 OFFSET 30Joins
Inner Join
const posts = await Post
.join('users', 'posts.user_id', '=', 'users.id')
.select('posts.*', 'users.name as author_name')
.get();Left / Right Join
await User
.leftJoin('orders', 'users.id', '=', 'orders.user_id')
.select('users.*', 'orders.total')
.get();
await Order.rightJoin('users', 'orders.user_id', '=', 'users.id').get();Cross Join
await Product.crossJoin('colors').get();Advanced Join Conditions
await User
.join('contacts', (join) => {
join.on('users.id', '=', 'contacts.user_id')
.orOn('users.id', '=', 'contacts.secondary_user_id');
})
.get();Sub-select Joins
const latestOrders = Order
.select('user_id')
.selectRaw('MAX(created_at) as last_order_at')
.groupBy('user_id');
await User
.joinSub(latestOrders, 'latest_orders', 'users.id', '=', 'latest_orders.user_id')
.get();Aggregates
const count = await User.count();
const count = await User.where('is_active', true).count('id');
const max = await User.max('age');
const min = await User.min('age');
const avg = await User.avg('score');
const sum = await User.sum('balance');
const found = await User.where('email', 'a@b.com').exists();Raw Expressions
Use raw expressions when you need database-specific SQL that the builder cannot produce.
import { raw } from '@wrsouza/orion';
// Raw in select
const users = await User
.selectRaw('name, UPPER(email) as email_upper')
.get();
// Raw in where
const users = await User
.whereRaw('LOWER(email) = ?', ['alice@example.com'])
.get();
// orWhereRaw
const users = await User
.where('is_active', true)
.orWhereRaw('age > ? AND country = ?', [18, 'BR'])
.get();
// Raw in having
await Order
.groupBy('user_id')
.havingRaw('SUM(total) > ?', [1000])
.get();
// Raw in orderBy
await User.orderByRaw('FIELD(status, "active", "pending", "inactive")').get();
// Inline raw expression (use sparingly — not parameterized)
const users = await User
.select(raw('COUNT(*) as total'), 'country')
.groupBy('country')
.get();Security: Always use
whereRawwith?placeholders and a parameter array. Never interpolate user input directly into raw strings.
Insert, Update, Delete
These methods execute immediately on the connection — they do not go through model events.
// Insert
await User.insert({ name: 'Alice', email: 'alice@example.com' });
await User.insert([
{ name: 'Alice', email: 'a@example.com' },
{ name: 'Bob', email: 'b@example.com' },
]);
// Insert and get the new ID
const id = await User.insertGetId({ name: 'Carol', email: 'c@example.com' });
// Update
await User.where('is_active', false).update({ status: 'archived' });
// Increment / Decrement
await User.where('id', 1).increment('login_count');
await User.where('id', 1).increment('balance', 50);
await User.where('id', 1).decrement('credits', 10);
// Delete
await User.where('created_at', '<', cutoffDate).delete();
// Truncate
await User.truncate();Subqueries in Select
Add a correlated subquery as a selected column:
const users = await User
.select('id', 'name')
.selectSub((q) => {
q.from('orders')
.selectRaw('COUNT(*)')
.whereColumn('orders.user_id', 'users.id');
}, 'orders_count')
.get();
// user.getRelation<number>('orders_count') → 5Or use addSubSelect:
const users = await User
.addSubSelect((q) => {
q.from('posts').selectRaw('MAX(created_at)').whereColumn('posts.user_id', 'users.id');
}, 'latest_post_at')
.get();Transactions
import { ConnectionManager } from '@wrsouza/orion';
const db = ConnectionManager.getConnection();
await db.transaction(async (trx) => {
await trx.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [100, 1]);
await trx.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [100, 2]);
// if an exception is thrown, the transaction is automatically rolled back
});Nested transactions use savepoints automatically:
await db.transaction(async (outer) => {
await outer.query('INSERT INTO logs (msg) VALUES ($1)', ['outer start']);
await outer.transaction(async (inner) => {
await inner.query('INSERT INTO logs (msg) VALUES ($1)', ['inner']);
// throwing here rolls back only to the savepoint, not the whole outer transaction
});
});