Fully-typed, model-driven Query Builder for Bun’s native sql
.
Define your data model once and get a type-safe query experience (a la Kysely/Laravel), powered by Bun’s tagged templates for safety and performance.
- Typed from Models: Infer tables/columns/PKs from your model files;
selectFrom('users')
andwhere({ active: true })
are typed. - Fluent Builder:
select/insert/update/delete
,where/andWhere/orWhere
,join/leftJoin/rightJoin/crossJoin
,groupBy/having
,union/unionAll
. - Aggregations:
count()
,avg()
,sum()
,max()
,min()
with full type safety. - Batch Operations:
insertMany()
,updateMany()
,deleteMany()
for efficient bulk operations.
- Relations:
with(...)
,withCount(...)
,whereHas(...)
,has()
,doesntHave()
,selectAllRelations()
with configurable aliasing and constraint callbacks. - Query Scopes: Define reusable query constraints on models for cleaner, more maintainable code.
- Query Caching: Built-in LRU cache with TTL support via
cache(ttlMs)
,clearQueryCache()
,setQueryCacheMaxSize()
. - Model Hooks: Lifecycle events -
beforeCreate
,afterCreate
,beforeUpdate
,afterUpdate
,beforeDelete
,afterDelete
.
- Utilities:
distinct/distinctOn
,orderByDesc/latest/oldest/inRandomOrder
,whereColumn/whereRaw/groupByRaw/havingRaw
, JSON/date helpers. - Pagination:
paginate
,simplePaginate
,cursorPaginate
, pluschunk/chunkById/eachById
. - Soft Deletes:
withTrashed()
,onlyTrashed()
for logical deletion support.
- Transactions:
transaction
with retries/backoff/isolation/onRetry/afterCommit;savepoint
; distributed tx helpers. - Migrations: Generate and execute migrations from models with full diff support.
- Seeders: Database seeding with fake data generation via
ts-mocker
(faker alternative). - Raw Queries: Tagged templates and parameterized queries with
raw()
andunsafe()
.
- Configurable: Dialect hints, timestamps, alias strategies, relation FK formats, JSON mode, random function, shared lock syntax.
- Bun API passthroughs:
unsafe
,file
,simple
, poolreserve/release
,close
,ping/waitForReady
. - CLI: Introspection, query printing, connectivity checks, file/unsafe execution, explain.
Note: LISTEN/NOTIFY and COPY helpers are scaffolded and will be wired as Bun exposes native APIs.
bun-query-builder is built for speed. Benchmarked against popular TypeScript query builders and ORMs:
Operation | bun-query-builder | Kysely | Drizzle | Prisma |
---|---|---|---|---|
SELECT by ID | 14.3 µs | N/A | 33.2 µs (2.3x slower) | 82.4 µs (5.8x slower) |
SELECT with limit | 18.3 µs | N/A | 32.8 µs (1.8x slower) | 102 µs (5.6x slower) |
COUNT query | 12.6 µs | 37.6 µs (3x slower) | 113 µs (8.9x slower) | 86.9 µs (6.9x slower) |
Large result set (1000 rows) | 247 µs | N/A | 564 µs (2.3x slower) | 3,461 µs (14x slower) |
ORDER BY + LIMIT | 270 µs | N/A | 274 µs (1x slower) | 488 µs (1.8x slower) |
Key Performance Highlights:
- 🚀 2-6x faster than competitors for simple SELECT queries
- 🚀 Up to 14x faster than Prisma for large result sets
- 🚀 Up to 8.9x faster than Drizzle for COUNT operations
- ⚡ Leverages Bun's native SQL for optimal performance
bun add bun-query-builder
import { buildDatabaseSchema, buildSchemaMeta, createQueryBuilder } from 'bun-query-builder'
// Load or define your model files (see docs for model shape)
const models = {
User: { name: 'User', table: 'users', primaryKey: 'id', attributes: { id: { validation: { rule: {} } }, name: { validation: { rule: {} } }, active: { validation: { rule: {} } } } },
} as const
const schema = buildDatabaseSchema(models as any)
const meta = buildSchemaMeta(models as any)
const db = createQueryBuilder<typeof schema>({ schema, meta })
// Fully-typed query
const q = db
.selectFrom('users')
.where({ active: true })
.orderBy('created_at', 'desc')
.limit(10)
const rows = await q.execute()
// Get average age of active users
const avgAge = await db.selectFrom('users')
.where({ active: true })
.avg('age')
// Count total posts
const totalPosts = await db.selectFrom('posts').count()
// Get max and min scores
const maxScore = await db.selectFrom('users').max('score')
const minScore = await db.selectFrom('users').min('score')
// Insert multiple records at once
await db.insertMany('users', [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' },
{ name: 'Charlie', email: 'charlie@example.com' },
])
// Update multiple records matching conditions
await db.updateMany('users', { verified: false }, { status: 'pending' })
// Delete multiple records by IDs
await db.deleteMany('users', [1, 2, 3, 4, 5])
// Cache query results for 60 seconds (default)
const users = await db.selectFrom('users')
.where({ active: true })
.cache()
.get()
// Custom cache TTL (5 seconds)
const posts = await db.selectFrom('posts')
.orderBy('created_at', 'desc')
.limit(10)
.cache(5000)
.get()
// Clear all cached queries
clearQueryCache()
// Configure cache size
setQueryCacheMaxSize(500)
const db = createQueryBuilder<typeof schema>({
schema,
meta,
hooks: {
beforeCreate: async ({ table, data }) => {
console.log(`Creating ${table}:`, data)
// Modify data, validate, or throw to prevent creation
},
afterCreate: async ({ table, data, result }) => {
console.log(`Created ${table}:`, result)
// Trigger notifications, update caches, etc.
},
beforeUpdate: async ({ table, data, where }) => {
// Audit logging, validation, etc.
},
afterUpdate: async ({ table, data, where, result }) => {
// Clear related caches, send webhooks, etc.
},
beforeDelete: async ({ table, where }) => {
// Prevent deletion, check constraints, etc.
},
afterDelete: async ({ table, where, result }) => {
// Clean up related data, update aggregates, etc.
},
}
})
// Define scopes on your models
const User = {
name: 'User',
table: 'users',
scopes: {
active: (qb) => qb.where({ status: 'active' }),
verified: (qb) => qb.where({ email_verified_at: ['IS NOT', null] }),
premium: (qb) => qb.where({ subscription: 'premium' }),
},
// ... other model properties
}
// Use scopes in queries
const activeUsers = await db.selectFrom('users')
.scope('active')
.scope('verified')
.get()
// Eager load with constraints
const users = await db.selectFrom('users')
.with({
posts: (qb) => qb.where('published', '=', true).orderBy('created_at', 'desc')
})
.get()
// Check for related records
const usersWithPosts = await db.selectFrom('users')
.has('posts')
.get()
// Query by relationship existence
const activeAuthors = await db.selectFrom('users')
.whereHas('posts', (qb) => qb.where('published', '=', true))
.get()
Generate and execute migrations from your models:
import { generateMigration, executeMigration } from 'bun-query-builder'
// Generate migration from models directory
const migration = await generateMigration('./models', {
dialect: 'postgres',
apply: true,
full: true
})
// Execute the migration
await executeMigration(migration)
Populate your database with test data using seeders powered by ts-mocker:
# Generate a new seeder
bun qb make:seeder User
# This creates database/seeders/UserSeeder.ts
import { Seeder } from 'bun-query-builder'
import { faker } from 'ts-mocker'
export default class UserSeeder extends Seeder {
async run(qb: any): Promise<void> {
// Generate 50 users with realistic fake data
const users = Array.from({ length: 50 }, () => ({
name: faker.person.fullName(),
email: faker.internet.email(),
age: faker.number.int(18, 80),
role: faker.helpers.arrayElement(['admin', 'user', 'moderator']),
created_at: new Date(),
updated_at: new Date(),
}))
await qb.table('users').insert(users).execute()
}
// Control execution order (lower runs first)
get order(): number {
return 10 // Default is 100
}
}
# Run all seeders
bun qb seed
bun qb db:seed
# Run a specific seeder
bun qb seed --class UserSeeder
# Drop all tables, re-run migrations and seed
bun qb db:fresh
import { runSeeders, runSeeder } from 'bun-query-builder'
// Run all seeders
await runSeeders({
seedersDir: './database/seeders',
verbose: true
})
// Run specific seeder
await runSeeder('UserSeeder', { verbose: true })
# Print inferred schema from model dir
query-builder introspect ./app/Models --verbose
# Print a sample SQL (text) for a table
query-builder sql ./app/Models users --limit 5
# Migrations
query-builder migrate ./app/Models --dialect postgres
query-builder migrate:fresh ./app/Models
query-builder reset ./app/Models
# Seeders
query-builder make:seeder User
query-builder seed
query-builder db:seed --class UserSeeder
query-builder db:fresh
# Connectivity:
query-builder ping
query-builder wait-ready --attempts 30 --delay 250
# Execute a file or unsafe string (be careful!)
query-builder file ./migrations/seed.sql
query-builder unsafe "SELECT * FROM users WHERE id = $1" --params "[1]"
# Explain a query
query-builder explain "SELECT * FROM users WHERE active = true"
bun test
Please see our releases page for more information on what has changed recently.
Please see CONTRIBUTING for details.
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
For casual chit-chat with others using this package:
Join the Stacks Discord Server
“Software that is free, but hopes for a postcard.” We love receiving postcards from around the world showing where Stacks is being used! We showcase them on our website too.
Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎
We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.
The MIT License (MIT). Please see LICENSE for more information.
Made with 💙