Query Builder
The Bunty ORM Query Builder provides a fluent, type-safe interface for constructing complex database queries. Built on top of Bunโs native SQL drivers, it offers full TypeScript integration with automatic type inference and database-specific optimizations.
Overview
The Query Builder combines the power of raw SQL with the safety and convenience of an ORM:
- Type Safety: Full TypeScript integration with compile-time query validation
- Fluent Interface: Chainable methods for intuitive query construction
- Database Agnostic: Write once, run on MySQL, PostgreSQL, or SQLite
- Performance: Compiles to optimized native driver calls
- IntelliSense Support: Full autocompletion for tables, columns, and methods
- Raw SQL Escape Hatch: Drop down to raw SQL when needed
Basic Query Structure
All queries follow a consistent pattern across database types:
import { db } from './db';
// Basic select with type inference
const users = await db
.select()
.from('users')
.where('active', '=', true)
.orderBy('created_at', 'desc')
.limit(10);
// TypeScript knows the shape of users[]
users.forEach(user => {
console.log(user.id, user.name, user.email);
}); SELECT Queries
The SELECT query builder offers comprehensive functionality for data retrieval:
Basic Selection
// Select all columns
const users = await db.select().from('users');
// Select specific columns
const users = await db
.select(['id', 'name', 'email'])
.from('users');
// Select with aliases
const users = await db
.select({
userId: 'id',
fullName: 'name',
emailAddress: 'email',
isActive: 'active'
})
.from('users');
// Select with expressions
const stats = await db
.select({
total: db.raw('COUNT(*)'),
avgAge: db.raw('AVG(age)'),
maxSalary: db.raw('MAX(salary)')
})
.from('users'); Advanced Selection
// Group by with aggregates
const userStats = await db
.select({
department: 'department',
totalEmployees: db.raw('COUNT(*)'),
avgSalary: db.raw('AVG(salary)'),
maxSalary: db.raw('MAX(salary)'),
minSalary: db.raw('MIN(salary)')
})
.from('employees')
.groupBy('department')
.having(db.raw('COUNT(*)'), '>', 5)
.orderBy('avgSalary', 'desc');
// Window functions (PostgreSQL/MySQL 8.0+)
const rankedUsers = await db
.select({
id: 'id',
name: 'name',
salary: 'salary',
rank: db.raw('ROW_NUMBER() OVER (ORDER BY salary DESC)'),
departmentRank: db.raw('RANK() OVER (PARTITION BY department ORDER BY salary DESC)')
})
.from('employees');
// Subqueries
const highEarners = await db
.select()
.from('users')
.where('salary', '>', builder => {
builder
.select(db.raw('AVG(salary)'))
.from('users')
.where('department', '=', 'engineering');
}); INSERT Operations
The INSERT builder supports single records, bulk inserts, and upsert operations:
// Basic insert
const user = await db
.insert({
name: 'John Doe',
email: 'john@example.com',
active: true,
created_at: new Date()
})
.into('users');
// Insert with returning
const newUser = await db
.insert({
name: 'Jane Smith',
email: 'jane@example.com'
})
.into('users')
.returning('*');
// Insert with specific columns returned
const userId = await db
.insert({
name: 'Bob Wilson',
email: 'bob@example.com'
})
.into('users')
.returning('id');
// Insert with default values
const user = await db
.insert({
name: 'Alice Johnson',
email: 'alice@example.com'
// created_at will use database default
})
.into('users')
.returning(['id', 'created_at']); UPDATE Operations
UPDATE queries support conditional updates, joins, and atomic operations:
// Simple update
await db
.update('users')
.set({
name: 'Updated Name',
updated_at: new Date()
})
.where('id', '=', userId);
// Update with expressions
await db
.update('users')
.set({
login_count: db.raw('login_count + 1'),
last_login: new Date(),
updated_at: db.raw('NOW()')
})
.where('id', '=', userId);
// Conditional update
await db
.update('posts')
.set({ status: 'published', published_at: new Date() })
.where('status', '=', 'draft')
.where('scheduled_for', '<=', new Date());
// Update with returning
const updatedUser = await db
.update('users')
.set({ last_seen: new Date() })
.where('id', '=', userId)
.returning(['id', 'name', 'last_seen']); DELETE Operations
DELETE operations support soft deletes, cascading deletes, and cleanup operations:
// Simple delete
await db
.delete()
.from('users')
.where('id', '=', userId);
// Delete with conditions
await db
.delete()
.from('posts')
.where('status', '=', 'draft')
.where('created_at', '<', thirtyDaysAgo);
// Delete with returning (PostgreSQL)
const deletedUsers = await db
.delete()
.from('users')
.where('active', '=', false)
.where('last_login', '<', oneYearAgo)
.returning(['id', 'email']);
// Delete with limit (MySQL)
await db
.delete()
.from('logs')
.where('created_at', '<', oneWeekAgo)
.orderBy('created_at', 'asc')
.limit(1000); Raw SQL and Advanced Features
When you need the full power of SQL, the query builder provides escape hatches:
// Raw expressions in SELECT
const results = await db
.select({
id: 'id',
name: 'name',
age: db.raw('YEAR(CURDATE()) - YEAR(birth_date)'),
rank: db.raw('ROW_NUMBER() OVER (ORDER BY salary DESC)'),
percentile: db.raw('PERCENT_RANK() OVER (ORDER BY salary)')
})
.from('employees');
// Raw WHERE conditions
const users = await db
.select()
.from('users')
.whereRaw('MATCH(name, bio) AGAINST(? IN BOOLEAN MODE)', [searchTerm])
.whereRaw('created_at >= DATE_SUB(NOW(), INTERVAL ? DAY)', [30]);
// Complex raw queries
const analyticsData = await db
.select(db.raw(`
DATE(created_at) as date,
COUNT(*) as total_orders,
SUM(total) as revenue,
AVG(total) as avg_order_value,
COUNT(DISTINCT user_id) as unique_customers
`))
.from('orders')
.whereRaw('created_at >= ?', [startDate])
.groupByRaw('DATE(created_at)')
.orderByRaw('DATE(created_at) DESC'); Query Optimization
The query builder includes several optimization features:
Query Analysis
// Analyze query performance
const query = db
.select()
.from('users')
.join('orders', 'users.id', '=', 'orders.user_id')
.where('users.active', '=', true)
.where('orders.total', '>', 100);
// Get execution plan
const plan = await query.explain();
console.log('Execution plan:', plan);
// Get actual execution statistics
const stats = await query.analyze();
console.log('Query stats:', stats);
// Debug query compilation
const compiledQuery = query.toSQL();
console.log('SQL:', compiledQuery.sql);
console.log('Bindings:', compiledQuery.bindings);
// Performance monitoring
const startTime = Date.now();
const results = await query;
const duration = Date.now() - startTime;
console.log(`Query completed in ${duration}ms`); Error Handling and Debugging
Comprehensive error handling and debugging capabilities:
// Basic error handling
try {
const user = await db
.select()
.from('users')
.where('id', '=', userId)
.first();
if (!user) {
throw new Error('User not found');
}
return user;
} catch (error) {
if (error.code === 'ER_NO_SUCH_TABLE') {
console.error('Table does not exist');
} else if (error.code === 'ER_DUP_ENTRY') {
console.error('Duplicate entry');
} else {
console.error('Database error:', error.message);
}
throw error;
}
// Constraint violation handling
try {
await db.insert({
email: 'user@example.com',
name: 'John Doe'
}).into('users');
} catch (error) {
if (error.constraint === 'users_email_unique') {
throw new Error('Email already exists');
}
if (error.constraint === 'users_name_not_null') {
throw new Error('Name is required');
}
throw error;
}
// Validation before database operations
async function createUser(userData: any) {
// Validate required fields
if (!userData.email || !userData.name) {
throw new Error('Email and name are required');
}
// Check if email already exists
const existing = await db
.select('id')
.from('users')
.where('email', '=', userData.email)
.first();
if (existing) {
throw new Error('Email already registered');
}
return db.insert(userData).into('users').returning('*');
} Performance Best Practices
Guidelines for optimal query performance:
Query Optimization Tips
-
Use Indexes Effectively
- Create indexes on frequently queried columns
- Use composite indexes for multi-column conditions
- Avoid over-indexing (impacts insert/update performance)
-
Limit Result Sets
- Always use
LIMITfor pagination - Select only needed columns
- Use
WHEREclauses to filter early
- Always use
-
Optimize JOIN Operations
- Ensure JOIN columns are indexed
- Use appropriate JOIN types
- Consider denormalization for read-heavy workloads
-
Batch Operations
- Use bulk inserts instead of multiple single inserts
- Batch updates when possible
- Use transactions for related operations
-
Query Structure
- Avoid
SELECT *in production - Use EXISTS instead of IN for large datasets
- Consider using WITH clauses for complex queries
- Avoid
Connection Management
- Use connection pooling in production
- Monitor connection pool utilization
- Set appropriate timeout values
- Close connections properly
Whatโs Next?
Now that you understand the Query Builder, explore these related topics:
- Table Schemas - Define type-safe database schemas
- Relationships - Work with related data
- Migrations - Manage database schema changes
- Performance Optimization - Advanced optimization techniques
- Raw SQL - When to use raw SQL vs Query Builder