ReviewOS

also looking at this

stacks/bun-query-builder

docs: the write-path pages documented an API that does not exist

#1066
Merged glennmichael123 wants to merge fix/docs-write-path into main
20 files +663 -240
docs/advanced/distributed.mdmodified+4-4
Changes to docs/advanced/distributed.md
@@ -100,7 +100,7 @@ async function processOrder(orderData: {
100100 const order = await tx.insertInto('orders').values({
101101 customer_id: orderData.customerId,
102102 status: 'processing',
103 total: orderData.items.reduce((sum, item) => sum + (item.price _ item.quantity), 0),
103 total: orderData.items.reduce((sum, item) => sum + (item.price * item.quantity), 0),
104104 created_at: new Date()
105105 }).returning('id').execute()
106106
@@ -123,7 +123,7 @@ async function processOrder(orderData: {
123123 // Stage 2: Process payment
124124 const paymentResult = await processPayment({
125125 token: orderData.paymentToken,
126 amount: orderData.items.reduce((sum, item) => sum + (item.price _ item.quantity), 0),
126 amount: orderData.items.reduce((sum, item) => sum + (item.price * item.quantity), 0),
127127 orderId
128128 })
129129
@@ -506,7 +506,7 @@ async function monitoredOnboarding(userData: any) {
506506// Cleanup job for stale transactions
507507class TransactionCleanupJob {
508508 async cleanupStaleTransactions() {
509 const staleThreshold = Date.now() - (24 _ 60 _ 60 _ 1000) // 24 hours
509 const staleThreshold = Date.now() - (24 * 60 * 60 * 1000) // 24 hours
510510
511511 const staleTransactions = await db
512512 .selectFrom('distributed_transactions')
@@ -551,7 +551,7 @@ class TransactionCleanupJob {
551551setInterval(async () => {
552552 const cleanup = new TransactionCleanupJob()
553553 await cleanup.cleanupStaleTransactions()
554}, 60 _ 60 * 1000) // Every hour
554}, 60 * 60 * 1000) // Every hour
555555```
556556
557557## FAQ
docs/api/reference.mdmodified+6-6
Changes to docs/api/reference.md
@@ -64,7 +64,7 @@ const meta = buildSchemaMeta(models)
6464| `whereIn(col, vals)` | WHERE IN | `.whereIn('id', [1, 2, 3])` |
6565| `whereNotIn(col, vals)` | WHERE NOT IN | `.whereNotIn('status', ['banned'])` |
6666| `whereBetween(col, a, b)` | WHERE BETWEEN | `.whereBetween('age', 18, 65)` |
67| `whereNull(col)` | WHERE IS NULL | `.whereNull('deleted*at')` |
67| `whereNull(col)` | WHERE IS NULL | `.whereNull('deleted_at')` |
6868| `whereNotNull(col)` | WHERE IS NOT NULL | `.whereNotNull('email')` |
6969| `whereColumn(a, op, b)` | Compare columns | `.whereColumn('a', '=', 'b')` |
7070| `whereRaw(sql)` | Raw WHERE clause | `.whereRaw('LOWER(name) = ?', ['john'])` |
@@ -73,7 +73,7 @@ const meta = buildSchemaMeta(models)
7373
7474| Method | Description | Example |
7575|--------|-------------|---------|
76| `join(table, a, op, b)` | INNER JOIN | `.join('posts', 'users.id', '=', 'posts.user*id')` |
76| `join(table, a, op, b)` | INNER JOIN | `.join('posts', 'users.id', '=', 'posts.user_id')` |
7777| `leftJoin(...)` | LEFT JOIN | `.leftJoin('posts', ...)` |
7878| `rightJoin(...)` | RIGHT JOIN | `.rightJoin('posts', ...)` |
7979| `crossJoin(table)` | CROSS JOIN | `.crossJoin('categories')` |
@@ -83,8 +83,8 @@ const meta = buildSchemaMeta(models)
8383| Method | Description | Example |
8484|--------|-------------|---------|
8585| `orderBy(col, dir)` | ORDER BY | `.orderBy('name', 'asc')` |
86| `orderByDesc(col)` | ORDER BY DESC | `.orderByDesc('created*at')` |
87| `latest()` | Order by created*at DESC | `.latest()` |
86| `orderByDesc(col)` | ORDER BY DESC | `.orderByDesc('created_at')` |
87| `latest()` | Order by created_at DESC | `.latest()` |
8888| `oldest()` | Order by created_at ASC | `.oldest()` |
8989| `inRandomOrder()` | Random order | `.inRandomOrder()` |
9090| `limit(n)` | LIMIT | `.limit(10)` |
@@ -127,7 +127,7 @@ const meta = buildSchemaMeta(models)
127127
128128| Method | Description | Example |
129129|--------|-------------|---------|
130| `update(table)` | Start UPDATE | `db.update('users')` |
130| `update(table)` | Start UPDATE | `db.updateTable('users')` |
131131| `set(data)` | Set values | `.set({ active: false })` |
132132| `increment(col, n)` | Increment | `.increment('views', 1)` |
133133| `decrement(col, n)` | Decrement | `.decrement('stock', 1)` |
@@ -196,7 +196,7 @@ Execute queries in a transaction.
196196```typescript
197197await db.transaction(async (trx) => {
198198 await trx.insertInto('users').values({...}).execute()
199 await trx.update('accounts').set({...}).execute()
199 await trx.updateTable('accounts').set({...}).execute()
200200})
201201```
202202
docs/delete.mdmodified+84-16
Changes to docs/delete.md
@@ -2,6 +2,67 @@
22title: Delete Queries
33description: Delete records from your database with type-safe queries.
44---
5
6# Delete Queries
7
8Delete records with type-safe queries, soft deletes, and cascade support.
9
10## Basic Delete
11
12```typescript
13import { createQueryBuilder } from 'bun-query-builder'
14
15const db = createQueryBuilder<typeof schema>({ schema, meta })
16
17// Delete by ID
18await db.remove('users', 1)
19
20// Delete with where clause
21await db
22 .deleteFrom('users')
23 .where({ active: false })
24 .execute()
25```
26
27## Delete with Conditions
28
29```typescript
30// Delete records matching conditions
31await db
32 .deleteFrom('posts')
33 .where('created_at', '<', '2023-01-01')
34 .execute()
35
36// Delete with multiple conditions
37await db
38 .deleteFrom('sessions')
39 .where('expired', '=', true)
40 .andWhere('last_activity', '<', '2024-01-01')
41 .execute()
42```
43
44## Delete Many
45
46```typescript
47// Delete multiple records by IDs
48await db.deleteMany('users', [1, 2, 3, 4, 5])
49
50// Delete many with conditions
51await db
52 .deleteFrom('logs')
53 .where('level', '=', 'debug')
54 .where('created_at', '<', '2024-01-01')
55 .execute()
56```
57
58## Soft Deletes
59
60If your model supports soft deletes, records are marked as deleted instead of being removed:
61
62```typescript
63// Model with soft deletes enabled
64const models = {
65 User: {
566 name: 'User',
667 table: 'users',
768 softDeletes: true, // Enable soft deletes
@@ -10,7 +71,7 @@ description: Delete records from your database with type-safe queries.
1071}
1172
1273// Soft delete a record
13await db.delete('users', 1)
74await db.remove('users', 1)
1475// Sets deleted_at = NOW() instead of removing the record
1576
1677// Query excluding soft deleted records (default behavior)
@@ -34,13 +95,15 @@ const deletedUsers = await db
3495
3596```typescript
3697
37// Restore a soft deleted record
38await db.restore('users', 1)
98// Restoring is a model-layer operation: load the trashed row, then restore it.
99const user = await User.withTrashed().find(1)
100await user.restore()
39101// Sets deleted_at = NULL
40102
41// Restore with conditions
103// From the query builder, clear the soft-delete column directly
42104await db
43 .restoreFrom('users')
105 .updateTable('users')
106 .set({ deleted_at: null })
44107 .where({ email: 'restored@example.com' })
45108 .execute()
46109
@@ -50,8 +113,9 @@ await db
50113
51114```typescript
52115
53// Permanently delete a soft deleted record
54await db.forceDelete('users', 1)
116// Permanently delete a soft deleted record — `remove()` issues a real DELETE,
117// so it removes the row whether or not it was already soft-deleted.
118await db.remove('users', 1)
55119
56120// Force delete with conditions
57121await db
@@ -106,11 +170,15 @@ Remove all records from a table:
106170
107171```typescript
108172
109// Truncate entire table
110await db.truncate('logs')
173// Truncate entire table — a model-layer static
174await Log.truncate()
175
176// From the query builder, issue the statement directly.
177// TRUNCATE is not supported by SQLite; use DELETE there.
178await db.unsafe('TRUNCATE TABLE logs')
111179
112180// With cascade (if foreign keys exist)
113await db.truncate('users', { cascade: true })
181await db.unsafe('TRUNCATE TABLE users CASCADE')
114182
115183```
116184
@@ -126,7 +194,7 @@ await db.transaction(async (trx) => {
126194 .execute()
127195
128196 // Then delete the user
129 await trx.delete('users', userId)
197 await trx.remove('users', userId)
130198
131199 // All or nothing - if any delete fails, all are rolled back
132200})
@@ -149,7 +217,7 @@ async function deleteUserWithRelations(userId: number) {
149217 await trx.deleteFrom('posts').where({ user_id: userId }).execute()
150218
151219 // Delete user
152 await trx.delete('users', userId)
220 await trx.remove('users', userId)
153221 })
154222}
155223
@@ -225,15 +293,15 @@ async function cleanupData() {
225293 .where('created_at', '<', '2023-01-01')
226294 .execute()
227295
228 // Restore a user
229 await db.restore('users', 1)
296 // Restore a user (clear the soft-delete column)
297 await db.updateTable('users').set({ deleted_at: null }).where({ id: 1 }).execute()
230298
231299 // Query with trashed
232300 const allUsers = await db.selectFrom('users').withTrashed().get()
233301 const deletedOnly = await db.selectFrom('users').onlyTrashed().get()
234302
235 // Force delete
236 await db.forceDelete('users', [2, 3, 4])
303 // Permanently delete — a real DELETE, regardless of soft-delete state
304 await db.deleteMany('users', [2, 3, 4])
237305
238306 console.log('Cleanup completed')
239307}
docs/features/builder.mdmodified+4-4
Changes to docs/features/builder.md
@@ -707,7 +707,7 @@ await db
707707// Delete with complex conditions
708708await db
709709 .deleteFrom('sessions')
710 .where(['created_at', '<', new Date(Date.now() - 24 _ 60 _ 60 _ 1000)]) // older than 24h
710 .where(['created_at', '<', new Date(Date.now() - 24 * 60 * 60 * 1000)]) // older than 24h
711711 .andWhere(['user_id', 'is not', null])
712712 .execute()
713713
@@ -765,7 +765,7 @@ See the Pagination page for details. Highlights:
765765await db.selectFrom('users').paginate(25, 1)
766766await db.selectFrom('users').simplePaginate(25)
767767await db.selectFrom('users').cursorPaginate(100, undefined, 'id', 'asc')
768await db.selectFrom('users').chunkById(1000, 'id', async (batch) => { /_ ... */ })
768await db.selectFrom('users').chunkById(1000, 'id', async (batch) => { /* ... */ })
769769```
770770
771771## CTEs and Recursive Queries
@@ -1139,7 +1139,7 @@ const projectsWithCollaborators = await db
11391139```ts
11401140// Cleanup old sessions
11411141async function cleanupOldSessions() {
1142 const cutoffDate = new Date(Date.now() - 30 _ 24 _ 60 _ 60 _ 1000) // 30 days ago
1142 const cutoffDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // 30 days ago
11431143
11441144 const deletedCount = await db
11451145 .deleteFrom('user_sessions')
@@ -1159,7 +1159,7 @@ async function archiveCompletedProjects() {
11591159 .selectFrom('projects')
11601160 .select('projects', 'id', 'name')
11611161 .where(['status', '=', 'completed'])
1162 .where(['completed_at', '<', new Date(Date.now() - 6 _ 30 _ 24 _ 60 _ 60 _ 1000)])
1162 .where(['completed_at', '<', new Date(Date.now() - 6 * 30 * 24 * 60 * 60 * 1000)])
11631163 .execute()
11641164
11651165 if (projectsToArchive.length === 0)
docs/features/cli.mdmodified+10-10
Changes to docs/features/cli.md
@@ -725,8 +725,8 @@ query-builder file ./migrations/seed.sql
725725query-builder file ./scripts/cleanup.sql --params "[30]" # 30 days retention
726726
727727# Execute parameterized queries (use with caution)
728query-builder unsafe "SELECT _ FROM users WHERE team = $1" --params "[\"Engineering\"]"
729query-builder unsafe "SELECT _ FROM projects WHERE owner = $1 AND status = $2" --params "[\"Chris\",\"active\"]"
728query-builder unsafe "SELECT * FROM users WHERE team = $1" --params "[\"Engineering\"]"
729query-builder unsafe "SELECT * FROM projects WHERE owner = $1 AND status = $2" --params "[\"Chris\",\"active\"]"
730730```
731731
732732### Performance Analysis
@@ -738,12 +738,12 @@ query-builder benchmark --iterations 5000
738738query-builder benchmark --operations select,insert,count
739739
740740# Analyze query performance
741query-builder explain "SELECT _ FROM users WHERE active = true"
741query-builder explain "SELECT * FROM users WHERE active = true"
742742query-builder explain "SELECT u._, p.name as project_name FROM users u JOIN projects p ON p.user_id = u.id WHERE u.role = 'admin'"
743743
744744# Compare different query strategies
745query-builder explain "SELECT _ FROM users WHERE name LIKE '%Chris%'"
746query-builder explain "SELECT _ FROM users WHERE name ILIKE 'chris%'" # PostgreSQL
745query-builder explain "SELECT * FROM users WHERE name LIKE '%Chris%'"
746query-builder explain "SELECT * FROM users WHERE name ILIKE 'chris%'" # PostgreSQL
747747```
748748
749749## Best Practices
@@ -778,7 +778,7 @@ query-builder introspect ./app/Models --json | jq '.tables | keys'
778778query-builder sql ./app/Models users --where '{"team":"Chris Team"}' --limit 5
779779
780780# Good: Check performance impact
781query-builder explain "SELECT _ FROM users u JOIN projects p ON p.user_id = u.id WHERE u.active = true"
781query-builder explain "SELECT * FROM users u JOIN projects p ON p.user_id = u.id WHERE u.active = true"
782782query-builder benchmark --operations select,join
783783
784784# Good: Manage cache during development
@@ -786,7 +786,7 @@ query-builder cache:clear
786786query-builder cache:stats
787787
788788# Avoid: Direct SQL without parameterization
789query-builder unsafe "SELECT _ FROM users WHERE name = 'Chris'" # Risky!
789query-builder unsafe "SELECT * FROM users WHERE name = 'Chris'" # Risky!
790790
791791# Better: Use parameters
792792query-builder unsafe "SELECT * FROM users WHERE name = $1" --params '["Chris"]'
@@ -882,7 +882,7 @@ if [[ ! "$USER_ID" =~ ^[0-9]+$ ]]; then
882882 echo "Invalid user ID"
883883 exit 1
884884fi
885query-builder unsafe "SELECT _ FROM users WHERE id = $1" --params "[\"$USER_ID\"]"
885query-builder unsafe "SELECT * FROM users WHERE id = $1" --params "[\"$USER_ID\"]"
886886
887887# Production audit logging
888888echo "$(date): CLI operation by $(whoami): $_" >> /var/log/query-builder.log
@@ -904,7 +904,7 @@ query-builder ping || echo "Database unreachable - check network/credentials"
904904
905905# Debugging performance issues
906906query-builder benchmark --iterations 100
907query-builder explain "SELECT _ FROM large_table WHERE unindexed_column = 'value'"
907query-builder explain "SELECT * FROM large_table WHERE unindexed_column = 'value'"
908908
909909# Debugging schema mismatches
910910query-builder validate:schema ./app/Models
@@ -974,5 +974,5 @@ query-builder file ./migrations/seed.sql --params "[\"role\", \"admin\"]"
974974### Explain a complex statement
975975
976976```bash
977query-builder explain "SELECT _ FROM users WHERE active = true ORDER BY created_at DESC LIMIT 10"
977query-builder explain "SELECT * FROM users WHERE active = true ORDER BY created_at DESC LIMIT 10"
978978```
docs/features/pagination.mdmodified+31-31
Changes to docs/features/pagination.md
@@ -64,12 +64,12 @@ Implementation details:
6464
6565### Composite cursors
6666
67You can pass multiple columns for deterministic ordering, e.g. `['created*at', 'id']`.
67You can pass multiple columns for deterministic ordering, e.g. `['created_at', 'id']`.
6868
6969```ts
7070const page = await db
7171 .selectFrom('users')
72 .cursorPaginate(50, undefined, ['created*at', 'id'], 'asc')
72 .cursorPaginate(50, undefined, ['created_at', 'id'], 'asc')
7373
7474// use page.meta.nextCursor to fetch the next window
7575```
@@ -126,9 +126,9 @@ await db.selectFrom('users').eachById(500, 'id', async (row) => {
126126async function getUsersPage(page: number = 1, perPage: number = 20) {
127127 const { data, meta } = await db
128128 .selectFrom('users')
129 .select('users', 'id', 'name', 'email', 'role', 'created*at')
129 .select('users', 'id', 'name', 'email', 'role', 'created_at')
130130 .where({ active: true })
131 .orderBy('created*at', 'desc')
131 .orderBy('created_at', 'desc')
132132 .paginate(perPage, page)
133133
134134 return {
@@ -148,7 +148,7 @@ async function getUsersPage(page: number = 1, perPage: number = 20) {
148148const chrisProjects = await db
149149 .selectFrom('projects')
150150 .where({ owner: 'Chris' })
151 .orderBy('updated*at', 'desc')
151 .orderBy('updated_at', 'desc')
152152 .paginate(10, 1)
153153```
154154
@@ -159,10 +159,10 @@ const chrisProjects = await db
159159async function getInfinitePosts(cursor?: string) {
160160 const page = await db
161161 .selectFrom('posts')
162 .select('posts', 'id', 'title', 'content', 'author*id', 'created*at')
162 .select('posts', 'id', 'title', 'content', 'author_id', 'created_at')
163163 .with('Author')
164164 .where({ published: true })
165 .cursorPaginate(20, cursor, 'created*at', 'desc')
165 .cursorPaginate(20, cursor, 'created_at', 'desc')
166166
167167 return {
168168 posts: page.data,
@@ -200,7 +200,7 @@ async function sendNewsletterToAllUsers() {
200200
201201 await db
202202 .selectFrom('users')
203 .where({ email*verified: true, newsletter*subscribed: true })
203 .where({ email_verified: true, newsletter_subscribed: true })
204204 .chunkById(100, 'id', async (userBatch) => {
205205 // Process batch of users
206206 for (const user of userBatch) {
@@ -220,15 +220,15 @@ async function exportUserData() {
220220 let exportedCount = 0
221221
222222 // Write CSV header
223 exportFile.write('id,name,email,created*at\n')
223 exportFile.write('id,name,email,created_at\n')
224224
225225 await db
226226 .selectFrom('users')
227 .select('users', 'id', 'name', 'email', 'created*at')
227 .select('users', 'id', 'name', 'email', 'created_at')
228228 .orderBy('id', 'asc')
229229 .chunk(500, async (userBatch) => {
230230 for (const user of userBatch) {
231 exportFile.write(`${user.id},"${user.name}","${user.email}","${user.created*at}"\n`)
231 exportFile.write(`${user.id},"${user.name}","${user.email}","${user.created_at}"\n`)
232232 exportedCount++
233233 }
234234
@@ -265,16 +265,16 @@ async function searchUsersWithPagination(query: string, page: number = 1) {
265265// Time-based cursor pagination for real-time feeds
266266async function getRealtimeFeed(cursor?: string, userId?: number) {
267267 let query = db
268 .selectFrom('feed*items')
269 .select('feed*items', 'id', 'content', 'created*at', 'user*id')
268 .selectFrom('feed_items')
269 .select('feed_items', 'id', 'content', 'created_at', 'user_id')
270270 .with('User')
271 .orderBy('created*at', 'desc')
271 .orderBy('created_at', 'desc')
272272
273273 if (userId) {
274 query = query.where({ user*id: userId })
274 query = query.where({ user_id: userId })
275275 }
276276
277 const page = await query.cursorPaginate(25, cursor, 'created*at', 'desc')
277 const page = await query.cursorPaginate(25, cursor, 'created_at', 'desc')
278278
279279 return {
280280 items: page.data,
@@ -308,12 +308,12 @@ const buddyFeed = await getRealtimeFeed(undefined, buddyUserId)
308308const page = await db
309309 .selectFrom('posts')
310310 .where({ published: true })
311 .cursorPaginate(50, cursor, 'created*at', 'desc') // created*at should be indexed
311 .cursorPaginate(50, cursor, 'created_at', 'desc') // created_at should be indexed
312312
313313// Good: Composite cursor for stable ordering
314314const page = await db
315315 .selectFrom('events')
316 .cursorPaginate(25, cursor, ['created*at', 'id'], 'desc') // Both columns indexed
316 .cursorPaginate(25, cursor, ['created_at', 'id'], 'desc') // Both columns indexed
317317
318318// Avoid: Large offset pagination
319319const badPage = await db
@@ -334,16 +334,16 @@ async function getLatestNotifications(userId: number, cursor?: string) {
334334 // Cursor pagination is stable even if new notifications are added
335335 return await db
336336 .selectFrom('notifications')
337 .where({ user*id: userId, deleted*at: null })
338 .cursorPaginate(20, cursor, ['created*at', 'id'], 'desc')
337 .where({ user_id: userId, deleted_at: null })
338 .cursorPaginate(20, cursor, ['created_at', 'id'], 'desc')
339339}
340340
341341// Composite cursor for handling duplicate timestamps
342342async function getOrderHistory(customerId: number, cursor?: string) {
343343 return await db
344344 .selectFrom('orders')
345 .where({ customer*id: customerId })
346 .cursorPaginate(25, cursor, ['created*at', 'id'], 'desc')
345 .where({ customer_id: customerId })
346 .cursorPaginate(25, cursor, ['created_at', 'id'], 'desc')
347347}
348348```
349349
@@ -374,12 +374,12 @@ interface PaginatedResponse<T> {
374374async function getUsersAPI(req: Request): Promise<PaginatedResponse<User>> {
375375 try {
376376 const page = Math.max(1, Number.parseInt(req.query.page as string) || 1)
377 const perPage = Math.min(100, Math.max(1, Number.parseInt(req.query.per*page as string) || 20))
377 const perPage = Math.min(100, Math.max(1, Number.parseInt(req.query.per_page as string) || 20))
378378
379379 const result = await db
380380 .selectFrom('users')
381381 .where({ active: true })
382 .orderBy('created*at', 'desc')
382 .orderBy('created_at', 'desc')
383383 .paginate(perPage, page)
384384
385385 return {
@@ -415,7 +415,7 @@ async function processLargeDataset(batchSize: number = 1000) {
415415
416416 try {
417417 await db
418 .selectFrom('large*table')
418 .selectFrom('large_table')
419419 .where(['id', '>', lastProcessedId])
420420 .orderBy('id', 'asc')
421421 .chunkById(batchSize, 'id', async (batch) => {
@@ -513,7 +513,7 @@ const UsersList = () => {
513513
514514### Can I use composite cursors
515515
516Yes. Provide an array of columns like `['created*at', 'id']`. The cursor will carry a tuple of values in order.
516Yes. Provide an array of columns like `['created_at', 'id']`. The cursor will carry a tuple of values in order.
517517
518518### How do I resume a chunked job
519519
@@ -530,7 +530,7 @@ let cursor: number | undefined
530530do {
531531 const { data, meta } = await db
532532 .selectFrom('events')
533 .cursorPaginate(100, cursor, 'created*at', 'desc')
533 .cursorPaginate(100, cursor, 'created_at', 'desc')
534534 // process new slice
535535 cursor = meta.nextCursor as number | undefined
536536} while (cursor)
@@ -551,15 +551,15 @@ const { data, meta } = await db
551551```ts
552552await db
553553 .selectFrom('posts')
554 .orderBy('published*at', 'desc')
554 .orderBy('published_at', 'desc')
555555 .paginate(10, 1)
556556```
557557
558558### Chunk with transformation
559559
560560```ts
561await db.selectFrom('audit*logs').chunk(5000, async (rows) => {
562 const normalized = rows.map(r => ({ ...r, ts: new Date(r.created*at) }))
561await db.selectFrom('audit_logs').chunk(5000, async (rows) => {
562 const normalized = rows.map(r => ({ ...r, ts: new Date(r.created_at) }))
563563 await writeLogs(normalized)
564564})
565565```
@@ -577,5 +577,5 @@ await db.selectFrom('images').eachById(200, 'id', async (row) => {
577577Ensure the cursor column has an index; otherwise, performance degrades.
578578
579579```sql
580CREATE INDEX IF NOT EXISTS idx*users*id ON users(id);
580CREATE INDEX IF NOT EXISTS idx_users_id ON users(id);
581581```
docs/features/relations.mdmodified+1-1
Changes to docs/features/relations.md
@@ -357,7 +357,7 @@ const buddyManagers = await db
357357const averyActiveTeam = await db
358358 .selectFrom('users')
359359 .whereHas('Team', ['lead_id', '=', (await db.selectFrom('users').where({ name: 'Avery' }).first())?.id])
360 .whereHas('Activity', ['created_at', '>', new Date(Date.now() - 7 _ 24 _ 60 _ 60 _ 1000)])
360 .whereHas('Activity', ['created_at', '>', new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)])
361361 .execute()
362362```
363363
docs/features/seeding.mdmodified+21-21
Changes to docs/features/seeding.md
@@ -47,8 +47,8 @@ export default class UserSeeder extends Seeder {
4747 const users = Array.from({ length: 10 }, () => ({
4848 name: faker.person.fullName(),
4949 email: faker.internet.email(),
50 created*at: new Date(),
51 updated*at: new Date(),
50 created_at: new Date(),
51 updated_at: new Date(),
5252 }))
5353
5454 await qb.table('users').insert(users).execute()
@@ -99,8 +99,8 @@ export default class ProductSeeder extends Seeder {
9999 description: faker.commerce.productDescription(),
100100 price: faker.commerce.price(10, 1000),
101101 stock: faker.number.int(0, 100),
102 created*at: new Date(),
103 updated*at: new Date(),
102 created_at: new Date(),
103 updated_at: new Date(),
104104 }))
105105
106106 await qb.table('products').insert(products).execute()
@@ -120,8 +120,8 @@ export default class UserSeeder extends Seeder {
120120 name: faker.person.fullName(),
121121 email: faker.internet.email(),
122122 age: faker.number.int(18, 80),
123 created*at: new Date(),
124 updated*at: new Date(),
123 created_at: new Date(),
124 updated_at: new Date(),
125125 }))
126126
127127 await qb.table('users').insert(users).execute()
@@ -148,12 +148,12 @@ export default class PostSeeder extends Seeder {
148148 for (const user of users) {
149149 for (let i = 0; i < 3; i++) {
150150 posts.push({
151 user*id: user.id,
151 user_id: user.id,
152152 title: faker.lorem.sentence(5),
153153 body: faker.lorem.paragraphs(3),
154154 published: faker.datatype.boolean(0.7), // 70% published
155 created*at: faker.date.past(),
156 updated*at: new Date(),
155 created_at: faker.date.past(),
156 updated_at: new Date(),
157157 })
158158 }
159159 }
@@ -181,8 +181,8 @@ export default class LargeDataSeeder extends Seeder {
181181 const batch = Array.from({ length: batchSize }, () => ({
182182 name: faker.person.fullName(),
183183 email: faker.internet.email(),
184 created*at: new Date(),
185 updated*at: new Date(),
184 created_at: new Date(),
185 updated_at: new Date(),
186186 }))
187187
188188 await qb.table('users').insert(batch).execute()
@@ -211,8 +211,8 @@ export default class ConditionalSeeder extends Seeder {
211211 const users = Array.from({ length: 10 }, () => ({
212212 name: faker.person.fullName(),
213213 email: faker.internet.email(),
214 created*at: new Date(),
215 updated*at: new Date(),
214 created_at: new Date(),
215 updated_at: new Date(),
216216 }))
217217
218218 await qb.table('users').insert(users).execute()
@@ -237,7 +237,7 @@ faker.person.jobTitle() // "Software Engineer"
237237
238238```typescript
239239faker.internet.email() // "john.doe@example.com"
240faker.internet.userName() // "john*doe123"
240faker.internet.userName() // "john_doe123"
241241faker.internet.url() // "https://example.com"
242242faker.internet.password() // "aB3$dF7!"
243243```
@@ -438,7 +438,7 @@ Use appropriate faker methods for each field:
438438 phone: faker.phone.number(), // Valid phone number
439439 age: faker.number.int(18, 80), // Reasonable age range
440440 country: faker.location.country(), // Real country names
441 created*at: faker.date.past(), // Past dates for created*at
441 created_at: faker.date.past(), // Past dates for created_at
442442}
443443```
444444
@@ -505,8 +505,8 @@ function createUser(overrides = {}) {
505505 email: faker.internet.email(),
506506 age: faker.number.int(18, 80),
507507 role: 'user',
508 created*at: new Date(),
509 updated*at: new Date(),
508 created_at: new Date(),
509 updated_at: new Date(),
510510 ...overrides,
511511 }
512512}
@@ -537,12 +537,12 @@ export default class UserRoleSeeder extends Seeder {
537537 const userRoles = users.flatMap(user =>
538538 faker.helpers.arrayElements(roles, faker.number.int(1, 3))
539539 .map(role => ({
540 user*id: user.id,
541 role*id: role.id,
540 user_id: user.id,
541 role_id: role.id,
542542 }))
543543 )
544544
545 await qb.table('user*roles').insert(userRoles).execute()
545 await qb.table('user_roles').insert(userRoles).execute()
546546 }
547547
548548 get order(): number {
@@ -556,7 +556,7 @@ export default class UserRoleSeeder extends Seeder {
556556```typescript
557557export default class EnvironmentSeeder extends Seeder {
558558 async run(qb: any): Promise<void> {
559 const env = process.env.NODE*ENV
559 const env = process.env.NODE_ENV
560560
561561 if (env === 'production') {
562562 console.log('Skipping seeding in production')
docs/features/transactions.mdmodified+2-2
Changes to docs/features/transactions.md
@@ -160,7 +160,7 @@ await createUser({ name: 'Alice' })
160160You can pass options to override defaults:
161161
162162```ts
163const createWithRetry = db.transactional(async (tx) => { /_ ... _/ }, { retries: 3 })
163const createWithRetry = db.transactional(async (tx) => { /* ... */ }, { retries: 3 })
164164```
165165
166166## Error Handling and Logging
@@ -168,7 +168,7 @@ const createWithRetry = db.transactional(async (tx) => { /_ ... _/ }, { retries:
168168Use `onRetry` and `logger` to observe transaction lifecycle.
169169
170170```ts
171await db.transaction(async (tx) => { /_ ... _/ }, {
171await db.transaction(async (tx) => { /* ... */ }, {
172172 logger: (e) => {
173173 if (e.type === 'retry')
174174 console.warn('retry', e.attempt)
docs/guide/getting-started.mdmodified+1-1
Changes to docs/guide/getting-started.md
@@ -127,7 +127,7 @@ await db.insertMany('users', [
127127```typescript
128128// Update by condition
129129await db
130 .update('users')
130 .updateTable('users')
131131 .set({ active: false })
132132 .where({ id: 1 })
133133 .execute()
docs/guide/insert-update-delete.mdmodified+14-17
Changes to docs/guide/insert-update-delete.md
@@ -86,7 +86,7 @@ await db
8686
8787```typescript
8888await db
89 .update('users')
89 .updateTable('users')
9090 .set({ active: false })
9191 .where({ id: 1 })
9292 .execute()
@@ -96,7 +96,7 @@ await db
9696
9797```typescript
9898await db
99 .update('users')
99 .updateTable('users')
100100 .set({
101101 name: 'Updated Name',
102102 email: 'new@example.com',
@@ -111,7 +111,7 @@ await db
111111```typescript
112112// Increment a value
113113await db
114 .update('products')
114 .updateTable('products')
115115 .set({
116116 views: db.raw('views + 1')
117117 })
@@ -120,7 +120,7 @@ await db
120120
121121// Decrement stock
122122await db
123 .update('products')
123 .updateTable('products')
124124 .set({
125125 stock: db.raw('stock - ?', [quantity])
126126 })
@@ -144,13 +144,9 @@ await db.updateMany(
144144
145145```typescript
146146await db
147 .update('products')
147 .updateTable('products')
148148 .set({
149 category_id: db.subquery(
150 db.selectFrom('categories')
151 .select(['id'])
152 .where({ name: 'Electronics' })
153 )
149 category_id: db.raw('(SELECT id FROM categories WHERE name = ?)', ['Electronics']),
154150 })
155151 .where({ type: 'gadget' })
156152 .execute()
@@ -191,8 +187,9 @@ await db.deleteMany('users', [1, 2, 3, 4, 5])
191187// Delete all records (use with caution!)
192188await db.deleteFrom('temp_data').execute()
193189
194// Truncate table (faster, resets auto-increment)
195await db.truncate('temp_data')
190// Truncate table (faster, resets auto-increment).
191// Not supported by SQLite — use the DELETE above there.
192await db.unsafe('TRUNCATE TABLE temp_data')
196193```
197194
198195## Soft Deletes
@@ -202,7 +199,7 @@ await db.truncate('temp_data')
202199```typescript
203200// Instead of deleting, set deleted_at
204201await db
205 .update('users')
202 .updateTable('users')
206203 .set({ deleted_at: new Date() })
207204 .where({ id: 1 })
208205 .execute()
@@ -221,7 +218,7 @@ const activeUsers = await db
221218
222219```typescript
223220await db
224 .update('users')
221 .updateTable('users')
225222 .set({ deleted_at: null })
226223 .where({ id: 1 })
227224 .execute()
@@ -276,7 +273,7 @@ async function processOrder(orderId: number) {
276273 await db.transaction(async (trx) => {
277274 // Update order status
278275 await trx
279 .update('orders')
276 .updateTable('orders')
280277 .set({
281278 status: 'processing',
282279 processed_at: new Date()
@@ -293,7 +290,7 @@ async function processOrder(orderId: number) {
293290 // Decrement stock for each item
294291 for (const item of items) {
295292 await trx
296 .update('products')
293 .updateTable('products')
297294 .set({
298295 stock: db.raw('stock - ?', [item.quantity])
299296 })
@@ -322,7 +319,7 @@ async function deactivateInactiveUsers(daysInactive: number) {
322319 cutoffDate.setDate(cutoffDate.getDate() - daysInactive)
323320
324321 const result = await db
325 .update('users')
322 .updateTable('users')
326323 .set({
327324 active: false,
328325 deactivated_at: new Date(),
docs/guide/transactions.mdmodified+69-46
Changes to docs/guide/transactions.md
@@ -17,7 +17,7 @@ await transaction(async (trx) => {
1717 .execute()
1818
1919 await trx
20 .update('users')
20 .updateTable('users')
2121 .set({ has_account: true })
2222 .where({ id: 1 })
2323 .execute()
@@ -77,20 +77,24 @@ await transaction(fn, { isolationLevel: 'serializable' })
7777
7878Create savepoints within a transaction:
7979
80`savepoint()` takes a callback, like `transaction()` does — there is no
81`rollbackToSavepoint()`. The savepoint is released when the callback returns and
82rolled back if it throws, so "roll back to the savepoint" means "let the
83callback throw and catch it outside".
84
8085```typescript
81await transaction(async (trx) => {
86await db.transaction(async (trx) => {
8287 await trx.insertInto('logs').values({ message: 'Step 1' }).execute()
8388
84 // Create savepoint
85 await trx.savepoint('step2')
86
8789 try {
88 await trx.insertInto('logs').values({ message: 'Step 2' }).execute()
89 // Something fails
90 throw new Error('Step 2 failed')
91 } catch (e) {
92 // Rollback to savepoint (step 1 is preserved)
93 await trx.rollbackToSavepoint('step2')
90 await trx.savepoint(async (sp) => {
91 await sp.insertInto('logs').values({ message: 'Step 2' }).execute()
92 // Something fails — only the savepoint's work is undone
93 throw new Error('Step 2 failed')
94 })
95 }
96 catch {
97 // Step 1 is preserved; the transaction itself is still alive
9498 await trx.insertInto('logs').values({ message: 'Step 2 fallback' }).execute()
9599 }
96100
@@ -98,26 +102,41 @@ await transaction(async (trx) => {
98102})
99103```
100104
101## Manual Transaction Control
105`savepoint()` must be called inside a transaction; calling it outside throws.
106
107## Commit and rollback
102108
103For more control over transaction lifecycle:
109There is no manual `beginTransaction()` / `commit()` / `rollback()` API. The
110callback form owns the lifecycle: return normally and the transaction commits,
111throw and it rolls back.
104112
105113```typescript
106const trx = await db.beginTransaction()
114await db.transaction(async (tx) => {
115 await tx.insertInto('users').values({ name: 'Test' }).execute()
116 await tx.updateTable('counters').set({ value: db.raw('value + 1') }).execute()
117 // committed when this callback returns
118})
119```
107120
108try {
109 await trx.insertInto('users').values({ name: 'Test' }).execute()
110 await trx.update('counters').set({ value: db.raw('value + 1') }).execute()
121To roll back deliberately, throw — then handle the error outside:
111122
112 // Explicitly commit
113 await trx.commit()
114} catch (error) {
115 // Explicitly rollback
116 await trx.rollback()
117 throw error
123```typescript
124try {
125 await db.transaction(async (tx) => {
126 await tx.insertInto('users').values({ name: 'Test' }).execute()
127 if (!ok)
128 throw new Error('rolling back')
129 })
130}
131catch (error) {
132 // the transaction has already rolled back by the time you get here
118133}
119134```
120135
136Use `tx`, not `db`, for every statement inside the callback. `db` holds a
137different connection, so work issued through it is not part of the transaction
138and will not be rolled back with it.
139
121140## Nested Transactions
122141
123142Handle nested transactions with savepoints:
@@ -133,7 +152,7 @@ await transaction(async (trx) => {
133152 }
134153 })
135154
136 await trx.update('inventory').set(/_ ... _/).execute()
155 await trx.updateTable('inventory').set(/* ... */).execute()
137156})
138157```
139158
@@ -149,7 +168,7 @@ try {
149168 throw new Error('Business logic error')
150169 }
151170
152 await trx.update('accounts').set({ balance: 500 }).execute()
171 await trx.updateTable('accounts').set({ balance: 500 }).execute()
153172 })
154173} catch (error) {
155174 console.error('Transaction failed:', error.message)
@@ -166,7 +185,7 @@ await transaction(
166185 async (trx) => {
167186 // This might fail due to lock contention
168187 await trx
169 .update('accounts')
188 .updateTable('accounts')
170189 .set({ balance: db.raw('balance - ?', [100]) })
171190 .where({ id: 1 })
172191 .execute()
@@ -205,14 +224,14 @@ async function transferMoney(
205224 }
206225
207226 await trx
208 .update('accounts')
227 .updateTable('accounts')
209228 .set({ balance: fromAccount.balance - amount })
210229 .where({ id: fromAccountId })
211230 .execute()
212231
213232 // Add to destination account
214233 await trx
215 .update('accounts')
234 .updateTable('accounts')
216235 .set({ balance: db.raw('balance + ?', [amount]) })
217236 .where({ id: toAccountId })
218237 .execute()
@@ -277,17 +296,17 @@ async function placeOrder(userId: number, items: OrderItem[]) {
277296
278297 // Reduce inventory
279298 await trx
280 .update('products')
299 .updateTable('products')
281300 .set({ stock: product.stock - item.quantity })
282301 .where({ id: item.productId })
283302 .execute()
284303
285 total += product.price _ item.quantity
304 total += product.price * item.quantity
286305 }
287306
288307 // Update order total
289308 await trx
290 .update('orders')
309 .updateTable('orders')
291310 .set({ total, status: 'confirmed' })
292311 .where({ id: order.id })
293312 .execute()
@@ -304,28 +323,32 @@ async function placeOrder(userId: number, items: OrderItem[]) {
304323
305324### Batch Processing with Checkpoints
306325
326Wrap each chunk in its own savepoint. A chunk that throws is undone on its own,
327and the surrounding transaction — and every chunk before it — survives.
328
307329```typescript
308330async function processBatch(records: any[]) {
309 await transaction(async (trx) => {
310 for (let i = 0; i < records.length; i++) {
311 // Create checkpoint every 100 records
312 if (i > 0 && i % 100 === 0) {
313 await trx.savepoint(`batch_${i}`)
314 }
331 const failed: number[] = []
332
333 await db.transaction(async (trx) => {
334 for (let i = 0; i < records.length; i += 100) {
335 const chunk = records.slice(i, i + 100)
315336
316337 try {
317 await processRecord(trx, records[i])
318 } catch (error) {
319 // On error, rollback to last checkpoint and skip
320 const checkpoint = Math.floor(i / 100) _ 100
321 if (checkpoint > 0) {
322 await trx.rollbackToSavepoint(`batch_${checkpoint}`)
323 }
324 console.error(`Failed at record ${i}:`, error)
325 throw error
338 await trx.savepoint(async (sp) => {
339 for (const record of chunk)
340 await processRecord(sp, record)
341 })
342 }
343 catch (error) {
344 // Only this chunk rolled back; earlier chunks are intact.
345 console.error(`Failed in chunk starting at ${i}:`, error)
346 failed.push(i)
326347 }
327348 }
328349 })
350
351 return failed
329352}
330353```
331354
docs/guide/update.mdmodified+22-22
Changes to docs/guide/update.md
@@ -8,7 +8,7 @@ Learn how to update existing data in your database using bun-query-builder.
88
99```typescript
1010await db
11 .update('users')
11 .updateTable('users')
1212 .set({ active: false })
1313 .where({ id: 1 })
1414 .execute()
@@ -18,7 +18,7 @@ await db
1818
1919```typescript
2020await db
21 .update('users')
21 .updateTable('users')
2222 .set({
2323 name: 'John Smith',
2424 email: 'john.smith@example.com',
@@ -34,7 +34,7 @@ Return the updated rows:
3434
3535```typescript
3636const updated = await db
37 .update('users')
37 .updateTable('users')
3838 .set({ status: 'verified' })
3939 .where({ email_verified: true })
4040 .returning(['id', 'name', 'status'])
@@ -49,7 +49,7 @@ console.log(`Updated ${updated.length} users`)
4949
5050```typescript
5151await db
52 .update('users')
52 .updateTable('users')
5353 .set({ subscription: 'expired' })
5454 .where('subscription_ends_at', '<', new Date())
5555 .andWhere('subscription', '!=', 'lifetime')
@@ -60,7 +60,7 @@ await db
6060
6161```typescript
6262await db
63 .update('posts')
63 .updateTable('posts')
6464 .set({ status: 'archived' })
6565 .whereIn('id', [1, 2, 3, 4, 5])
6666 .execute()
@@ -73,14 +73,14 @@ await db
7373```typescript
7474// Increment a column
7575await db
76 .update('posts')
76 .updateTable('posts')
7777 .increment('views', 1)
7878 .where({ id: 1 })
7979 .execute()
8080
8181// Increment by custom amount
8282await db
83 .update('users')
83 .updateTable('users')
8484 .increment('points', 100)
8585 .where({ id: userId })
8686 .execute()
@@ -91,14 +91,14 @@ await db
9191```typescript
9292// Decrement a column
9393await db
94 .update('products')
94 .updateTable('products')
9595 .decrement('stock', 1)
9696 .where({ id: productId })
9797 .execute()
9898
9999// Ensure non-negative
100100await db
101 .update('products')
101 .updateTable('products')
102102 .decrement('stock', quantity)
103103 .where({ id: productId })
104104 .where('stock', '>=', quantity)
@@ -125,7 +125,7 @@ await db.updateMany(
125125// Update multiple records with different values
126126for (const update of updates) {
127127 await db
128 .update('products')
128 .updateTable('products')
129129 .set({ price: update.newPrice })
130130 .where({ id: update.productId })
131131 .execute()
@@ -135,7 +135,7 @@ for (const update of updates) {
135135await db.transaction(async (trx) => {
136136 for (const update of updates) {
137137 await trx
138 .update('products')
138 .updateTable('products')
139139 .set({ price: update.newPrice })
140140 .where({ id: update.productId })
141141 .execute()
@@ -149,7 +149,7 @@ await db.transaction(async (trx) => {
149149
150150```typescript
151151await db
152 .update('users')
152 .updateTable('users')
153153 .set({
154154 login_count: sql`login_count + 1`,
155155 last_login: sql`NOW()`
@@ -163,7 +163,7 @@ await db
163163```typescript
164164// Update JSON field
165165await db
166 .update('users')
166 .updateTable('users')
167167 .set({
168168 preferences: sql`JSON_SET(preferences, '$.theme', 'dark')`
169169 })
@@ -212,7 +212,7 @@ const db = createQueryBuilder({
212212
213213```typescript
214214await db
215 .update('users')
215 .updateTable('users')
216216 .set({ deleted_at: new Date() })
217217 .where({ id: 1 })
218218 .execute()
@@ -222,7 +222,7 @@ await db
222222
223223```typescript
224224await db
225 .update('users')
225 .updateTable('users')
226226 .set({ deleted_at: null })
227227 .where({ id: 1 })
228228 .execute()
@@ -245,7 +245,7 @@ async function updateProfile(userId: number, updates: ProfileUpdate) {
245245 }
246246
247247 const user = await db
248 .update('users')
248 .updateTable('users')
249249 .set(sanitized)
250250 .where({ id: userId })
251251 .returning(['id', 'name', 'bio', 'avatar_url', 'website'])
@@ -263,7 +263,7 @@ async function archiveOldPosts(daysOld: number) {
263263 cutoffDate.setDate(cutoffDate.getDate() - daysOld)
264264
265265 const result = await db
266 .update('posts')
266 .updateTable('posts')
267267 .set({
268268 status: 'archived',
269269 archived_at: new Date()
@@ -296,7 +296,7 @@ async function processOrder(orderId: number, items: OrderItem[]) {
296296
297297 // Decrement stock
298298 await trx
299 .update('products')
299 .updateTable('products')
300300 .decrement('stock', item.quantity)
301301 .where({ id: item.product_id })
302302 .execute()
@@ -304,7 +304,7 @@ async function processOrder(orderId: number, items: OrderItem[]) {
304304
305305 // Update order status
306306 await trx
307 .update('orders')
307 .updateTable('orders')
308308 .set({ status: 'processing' })
309309 .where({ id: orderId })
310310 .execute()
@@ -322,7 +322,7 @@ async function updateWithVersion(
322322 currentVersion: number
323323) {
324324 const result = await db
325 .update(table)
325 .updateTable(table)
326326 .set({
327327 ...data,
328328 version: currentVersion + 1
@@ -348,10 +348,10 @@ async function updateWithVersion(
348348
349349```typescript
350350// Good: Targeted update with index
351await db.update('users').set({ active: false }).where({ id: 1 }).execute()
351await db.updateTable('users').set({ active: false }).where({ id: 1 }).execute()
352352
353353// Avoid: Table scan
354await db.update('users').set({ active: false }).where('name', 'LIKE', '%test%').execute()
354await db.updateTable('users').set({ active: false }).where('name', 'LIKE', '%test%').execute()
355355```
356356
357357## Next Steps
docs/insert.mdmodified+68-4
Changes to docs/insert.md
@@ -2,6 +2,70 @@
22title: Insert Queries
33description: Insert records into your database with type-safe queries.
44---
5
6# Insert Queries
7
8Insert single or multiple records with full type safety.
9
10## Basic Insert
11
12```typescript
13import { createQueryBuilder } from 'bun-query-builder'
14
15const db = createQueryBuilder<typeof schema>({ schema, meta })
16
17// Insert a single record
18await db.create('users', {
19 name: 'John Doe',
20 email: 'john@example.com',
21 active: true,
22})
23
24// Insert returns the inserted record (with auto-generated ID)
25const newUser = await db.create('users', {
26 name: 'Jane Doe',
27 email: 'jane@example.com',
28})
29
30console.log(newUser.id) // Auto-generated ID
31```
32
33## Insert Many Records
34
35Insert multiple records efficiently:
36
37```typescript
38// Insert many records at once
39await db.insertMany('users', [
40 { name: 'Alice', email: 'alice@example.com' },
41 { name: 'Bob', email: 'bob@example.com' },
42 { name: 'Charlie', email: 'charlie@example.com' },
43])
44```
45
46## Insert with Timestamps
47
48Timestamps are automatically added if configured:
49
50```typescript
51// If your model has timestamps enabled
52await db.create('users', {
53 name: 'John Doe',
54 email: 'john@example.com',
55})
56// created_at and updated_at are auto-populated
57```
58
59## Insert or Ignore
60
61Insert only if the record doesn't exist (based on unique constraints):
62
63```typescript
64// Insert or ignore on conflict
65await db.insertOrIgnore('users', {
66 email: 'existing@example.com',
67 name: 'New Name',
68})
569```
670
771## Upsert (Insert or Update)
@@ -66,7 +130,7 @@ const db = createQueryBuilder<typeof schema>({
66130})
67131
68132// Hooks are called automatically
69await db.insert('users', {
133await db.create('users', {
70134 name: 'John Doe',
71135 email: 'john@example.com',
72136})
@@ -98,7 +162,7 @@ Insert related records:
98162```typescript
99163
100164// First insert the user
101const user = await db.insert('users', {
165const user = await db.create('users', {
102166 name: 'John Doe',
103167 email: 'john@example.com',
104168})
@@ -118,7 +182,7 @@ Wrap inserts in a transaction for atomicity:
118182```typescript
119183
120184await db.transaction(async (trx) => {
121 const user = await trx.insert('users', {
185 const user = await trx.create('users', {
122186 name: 'John Doe',
123187 email: 'john@example.com',
124188 })
@@ -178,7 +242,7 @@ const db = createQueryBuilder<typeof schema>({
178242// Insert operations
179243async function createUsers() {
180244 // Single insert
181 const admin = await db.insert('users', {
245 const admin = await db.create('users', {
182246 name: 'Admin User',
183247 email: 'admin@example.com',
184248 active: true,
docs/joins.mdmodified+64-0
Changes to docs/joins.md
@@ -2,6 +2,70 @@
22title: Joins
33description: Build type-safe JOIN queries with the query builder.
44---
5
6# Joins
7
8Build type-safe JOIN queries to combine data from multiple tables.
9
10## Inner Join
11
12Returns only records that have matching values in both tables:
13
14```typescript
15import { createQueryBuilder } from 'bun-query-builder'
16
17const db = createQueryBuilder<typeof schema>({ schema, meta })
18
19// Basic inner join
20const results = await db
21 .selectFrom('posts')
22 .join('users', 'posts.user_id', 'users.id')
23 .get()
24
25// With column selection
26const postsWithAuthors = await db
27 .selectFrom('posts')
28 .select(['posts.title', 'posts.content', 'users.name AS author'])
29 .join('users', 'posts.user_id', 'users.id')
30 .get()
31```
32
33## Left Join
34
35Returns all records from the left table and matching records from the right table:
36
37```typescript
38// Left join - get all users with their posts (if any)
39const usersWithPosts = await db
40 .selectFrom('users')
41 .select(['users.name', 'posts.title'])
42 .leftJoin('posts', 'users.id', 'posts.user_id')
43 .get()
44
45// Users without posts will have null for posts.title
46```
47
48## Right Join
49
50Returns all records from the right table and matching records from the left table:
51
52```typescript
53// Right join
54const postsWithUsers = await db
55 .selectFrom('posts')
56 .select(['posts.title', 'users.name'])
57 .rightJoin('users', 'posts.user_id', 'users.id')
58 .get()
59```
60
61## Cross Join
62
63Returns the Cartesian product of both tables:
64
65```typescript
66// Cross join - combine every row from both tables
67const combinations = await db
68 .selectFrom('colors')
569 .crossJoin('sizes')
670 .get()
771
docs/raw-queries.mdmodified+91-28
Changes to docs/raw-queries.md
@@ -2,6 +2,69 @@
22title: Raw Queries
33description: Execute raw SQL queries when you need full control.
44---
5
6# Raw Queries
7
8Execute raw SQL queries when you need full control over the query structure.
9
10## Raw Select
11
12Execute a raw SELECT query:
13
14```typescript
15import { createQueryBuilder } from 'bun-query-builder'
16
17const db = createQueryBuilder<typeof schema>({ schema, meta })
18
19// Raw query with parameters
20const users = await db.raw(
21 'SELECT * FROM users WHERE active = ? AND age > ?',
22 [true, 18]
23)
24
25// Using named parameters
26const posts = await db.raw(
27 'SELECT * FROM posts WHERE user_id = $userId AND published = $published',
28 { userId: 1, published: true }
29)
30```
31
32## Raw Expressions in Queries
33
34Use raw expressions within the query builder:
35
36```typescript
37// Raw in select
38const results = await db
39 .selectFrom('users')
40 .selectRaw('COUNT(*) AS total, AVG(age) AS avg_age')
41 .get()
42
43// Raw in where
44const recentUsers = await db
45 .selectFrom('users')
46 .whereRaw('DATE(created_at) > DATE_SUB(NOW(), INTERVAL 30 DAY)')
47 .get()
48
49// Raw in order by
50const sorted = await db
51 .selectFrom('products')
52 .orderByRaw('price * quantity DESC')
53 .get()
54
55// Raw in group by
56const grouped = await db
57 .selectFrom('orders')
58 .select(['SUM(amount) AS total'])
59 .groupByRaw("strftime('%Y-%m', created_at)")
60 .get()
61
62// Raw in having
63const filtered = await db
64 .selectFrom('orders')
65 .select(['user_id', 'SUM(amount) AS total'])
66 .groupBy('user_id')
67 .havingRaw('SUM(amount) > 1000')
568 .get()
669
770```
@@ -16,7 +79,7 @@ For queries that cannot use parameterized values:
1679const result = await db.unsafe(`
1780 SELECT * FROM users
1881 WHERE email LIKE '%@example.com'
19 ORDER BY created*at DESC
82 ORDER BY created_at DESC
2083 LIMIT 10
2184`)
2285
@@ -33,13 +96,13 @@ Execute non-query SQL statements:
3396```typescript
3497
3598// Create an index
36await db.execute('CREATE INDEX idx*users*email ON users(email)')
99await db.unsafe('CREATE INDEX idx_users_email ON users(email)')
37100
38101// Update statistics
39await db.execute('ANALYZE users')
102await db.unsafe('ANALYZE users')
40103
41104// Truncate table
42await db.execute('TRUNCATE TABLE logs')
105await db.unsafe('TRUNCATE TABLE logs')
43106
44107```
45108
@@ -110,14 +173,14 @@ Get typed results from raw queries:
110173interface UserStats {
111174 country: string
112175 count: number
113 avg*age: number
176 avg_age: number
114177}
115178
116179const stats = await db.raw<UserStats[]>(`
117180 SELECT
118181 country,
119182 COUNT(*) AS count,
120 AVG(age) AS avg*age
183 AVG(age) AS avg_age
121184 FROM users
122185 GROUP BY country
123186 ORDER BY count DESC
@@ -125,7 +188,7 @@ const stats = await db.raw<UserStats[]>(`
125188
126189// stats is typed as UserStats[]
127190stats.forEach((s) => {
128 console.log(`${s.country}: ${s.count} users, avg age ${s.avg*age}`)
191 console.log(`${s.country}: ${s.count} users, avg age ${s.avg_age}`)
129192})
130193
131194```
@@ -137,7 +200,7 @@ Use prepared statements for repeated queries:
137200```typescript
138201
139202// Prepare a statement
140const stmt = db.prepare('SELECT * FROM users WHERE id = ?')
203const stmt = db.unsafe('SELECT * FROM users WHERE id = ?')
141204
142205// Execute multiple times efficiently
143206const user1 = await stmt.get([1])
@@ -158,16 +221,16 @@ Execute raw queries within transactions:
158221await db.transaction(async (trx) => {
159222 // Raw insert
160223 await trx.raw(
161 'INSERT INTO audit*log (action, user*id) VALUES (?, ?)',
224 'INSERT INTO audit_log (action, user_id) VALUES (?, ?)',
162225 ['login', userId]
163226 )
164227
165228 // Regular query builder
166 await trx.update('users', userId, { last*login: new Date() })
229 await trx.updateTable('users').set({ last_login: new Date() }).where({ id: userId })
167230
168231 // Raw update
169232 await trx.raw(
170 'UPDATE statistics SET login*count = login*count + 1 WHERE user*id = ?',
233 'UPDATE statistics SET login_count = login_count + 1 WHERE user_id = ?',
171234 [userId]
172235 )
173236})
@@ -180,8 +243,8 @@ Analyze query execution plans:
180243
181244```typescript
182245
183// Get query execution plan
184const explain = await db.explain('SELECT * FROM users WHERE active = true')
246// Get query execution plan — `explain()` terminates a builder chain
247const explain = await db.selectFrom('users').where({ active: true }).explain()
185248console.log(explain)
186249
187250// Using CLI
@@ -206,7 +269,7 @@ const models = {
206269 email: { validation: { rule: {} } },
207270 age: { validation: { rule: {} } },
208271 country: { validation: { rule: {} } },
209 created*at: { validation: { rule: {} } },
272 created_at: { validation: { rule: {} } },
210273 },
211274 },
212275}
@@ -220,26 +283,26 @@ async function getComplexAnalytics() {
220283 // Complex aggregation not easily expressible with query builder
221284 interface MonthlyStats {
222285 month: string
223 new*users: number
224 returning*users: number
225 total*active: number
286 new_users: number
287 returning_users: number
288 total_active: number
226289 }
227290
228291 const stats = await db.raw<MonthlyStats[]>(`
229 WITH monthly*users AS (
292 WITH monthly_users AS (
230293 SELECT
231 strftime('%Y-%m', created*at) AS month,
294 strftime('%Y-%m', created_at) AS month,
232295 id,
233 COUNT(*) OVER (PARTITION BY id) AS visit*count
296 COUNT(*) OVER (PARTITION BY id) AS visit_count
234297 FROM users
235 WHERE created*at >= date('now', '-12 months')
298 WHERE created_at >= date('now', '-12 months')
236299 )
237300 SELECT
238301 month,
239 SUM(CASE WHEN visit*count = 1 THEN 1 ELSE 0 END) AS new*users,
240 SUM(CASE WHEN visit*count > 1 THEN 1 ELSE 0 END) AS returning*users,
241 COUNT(*) AS total*active
242 FROM monthly*users
302 SUM(CASE WHEN visit_count = 1 THEN 1 ELSE 0 END) AS new_users,
303 SUM(CASE WHEN visit_count > 1 THEN 1 ELSE 0 END) AS returning_users,
304 COUNT(*) AS total_active
305 FROM monthly_users
243306 GROUP BY month
244307 ORDER BY month
245308 `)
@@ -250,7 +313,7 @@ async function getComplexAnalytics() {
250313 .selectRaw(`
251314 COUNT(*) AS total,
252315 COUNT(CASE WHEN active = 1 THEN 1 END) AS active,
253 AVG(age) AS avg*age
316 AVG(age) AS avg_age
254317 `)
255318 .first()
256319
@@ -259,8 +322,8 @@ async function getComplexAnalytics() {
259322 `
260323 SELECT
261324 country,
262 COUNT(*) AS user*count,
263 AVG(age) AS avg*age
325 COUNT(*) AS user_count,
326 AVG(age) AS avg_age
264327 FROM users
265328 WHERE active = ?
266329 GROUP BY country
docs/select.mdmodified+62-0
Changes to docs/select.md
@@ -2,6 +2,68 @@
22title: Select Queries
33description: Build type-safe SELECT queries with the query builder.
44---
5
6# Select Queries
7
8Build type-safe SELECT queries with full column and table inference.
9
10## Basic Select
11
12```typescript
13import { createQueryBuilder, buildDatabaseSchema, buildSchemaMeta } from 'bun-query-builder'
14
15const db = createQueryBuilder<typeof schema>({ schema, meta })
16
17// Select all columns from a table
18const users = await db.selectFrom('users').get()
19
20// Select specific columns
21const names = await db
22 .selectFrom('users')
23 .select(['name', 'email'])
24 .get()
25
26// Select with alias
27const data = await db
28 .selectFrom('users')
29 .select(['name AS userName', 'email AS userEmail'])
30 .get()
31```
32
33## Where Clauses
34
35```typescript
36// Simple where
37const activeUsers = await db
38 .selectFrom('users')
39 .where({ active: true })
40 .get()
41
42// Where with operator
43const adults = await db
44 .selectFrom('users')
45 .where('age', '>=', 18)
46 .get()
47
48// Multiple conditions (AND)
49const activeAdults = await db
50 .selectFrom('users')
51 .where({ active: true })
52 .andWhere('age', '>=', 18)
53 .get()
54
55// OR conditions
56const results = await db
57 .selectFrom('users')
58 .where({ role: 'admin' })
59 .orWhere({ role: 'moderator' })
60 .get()
61```
62
63## Ordering Results
64
65```typescript
66// Order ascending (default)
567const users = await db
668 .selectFrom('users')
769 .orderBy('name')
docs/update.mdmodified+105-23
Changes to docs/update.md
@@ -2,9 +2,75 @@
22title: Update Queries
33description: Update records in your database with type-safe queries.
44---
5
6# Update Queries
7
8Update records with type-safe queries and automatic timestamp handling.
9
10## Basic Update
11
12```typescript
13import { createQueryBuilder } from 'bun-query-builder'
14
15const db = createQueryBuilder<typeof schema>({ schema, meta })
16
17// Update a single record by ID
18await db
19 .updateTable('users')
20 .set({
21 name: 'Updated Name',
22 })
23 .where({ id: 1 })
24 .execute()
25
26// Update with where clause
27await db
28 .updateTable('users')
29 .set({ active: false })
30 .where({ email: 'old@example.com' })
31 .execute()
32```
33
34## Update Multiple Records
35
36```typescript
37// Update all matching records
38await db
39 .updateTable('users')
40 .set({ status: 'inactive' })
41 .where('last_login', '<', '2024-01-01')
42 .execute()
43
44// Update with multiple conditions
45await db
46 .updateTable('users')
47 .set({ verified: true })
48 .where({ active: true })
49 .andWhere({ email_verified: true })
50 .execute()
51```
52
53## Update Many by Condition
54
55```typescript
56// Update many records matching conditions
57await db.updateMany('users', { active: false }, { status: 'archived' })
58// Updates all users where active = false, setting status = 'archived'
59```
60
61## Increment and Decrement
62
63```typescript
64// Increment a column value
65await db
66 .updateTable('posts')
67 .increment('views', 1)
68 .where({ id: 123 })
69 .execute()
70
571// Decrement a column value
672await db
7 .updateFrom('products')
73 .updateTable('products')
874 .decrement('stock', 5)
975 .where({ id: 456 })
1076 .execute()
@@ -18,9 +84,13 @@ If your model has timestamps enabled, `updated_at` is automatically updated:
1884```typescript
1985
2086// updated_at is automatically set
21await db.update('users', 1, {
22 name: 'New Name',
23})
87await db
88 .updateTable('users')
89 .set({
90 name: 'New Name',
91 })
92 .where({ id: 1 })
93 .execute()
2494// Equivalent to: SET name = 'New Name', updated_at = NOW()
2595
2696```
@@ -32,7 +102,7 @@ Get the updated record back:
32102```typescript
33103
34104const updated = await db
35 .updateFrom('users')
105 .updateTable('users')
36106 .set({ name: 'Updated Name' })
37107 .where({ id: 1 })
38108 .returning(['id', 'name', 'updated_at'])
@@ -51,7 +121,7 @@ Update only if conditions are met:
51121
52122// Update only active users
53123const result = await db
54 .updateFrom('users')
124 .updateTable('users')
55125 .set({ newsletter_sent: true })
56126 .where({ active: true })
57127 .where({ newsletter_sent: false })
@@ -90,16 +160,20 @@ const db = createQueryBuilder<typeof schema>({
90160```typescript
91161
92162// Update a JSON column
93await db.update('users', 1, {
94 preferences: {
95 theme: 'dark',
96 notifications: true,
97 },
98})
163await db
164 .updateTable('users')
165 .set({
166 preferences: {
167 theme: 'dark',
168 notifications: true,
169 },
170 })
171 .where({ id: 1 })
172 .execute()
99173
100174// Update nested JSON (if supported by your database)
101175await db
102 .updateFrom('users')
176 .updateTable('users')
103177 .set({
104178 'preferences.theme': 'light',
105179 })
@@ -116,7 +190,7 @@ Efficiently update multiple records:
116190
117191// Update many records with the same values
118192await db
119 .updateFrom('posts')
193 .updateTable('posts')
120194 .set({ published: true, published_at: new Date() })
121195 .where('id', 'IN', [1, 2, 3, 4, 5])
122196 .execute()
@@ -129,13 +203,17 @@ await db
129203
130204await db.transaction(async (trx) => {
131205 // Update user
132 await trx.update('users', userId, {
133 status: 'premium',
134 })
206 await trx
207 .updateTable('users')
208 .set({
209 status: 'premium',
210 })
211 .where({ id: userId })
212 .execute()
135213
136214 // Update related subscription
137215 await trx
138 .updateFrom('subscriptions')
216 .updateTable('subscriptions')
139217 .set({ active: true, upgraded_at: new Date() })
140218 .where({ user_id: userId })
141219 .execute()
@@ -203,20 +281,24 @@ const db = createQueryBuilder<typeof schema>({
203281// Various update operations
204282async function updateUsers() {
205283 // Update single record
206 await db.update('users', 1, {
207 name: 'John Smith',
208 })
284 await db
285 .updateTable('users')
286 .set({
287 name: 'John Smith',
288 })
289 .where({ id: 1 })
290 .execute()
209291
210292 // Update with conditions
211293 await db
212 .updateFrom('users')
294 .updateTable('users')
213295 .set({ status: 'inactive' })
214296 .where('last_login', '<', '2024-01-01')
215297 .execute()
216298
217299 // Increment login count
218300 await db
219 .updateFrom('users')
301 .updateTable('users')
220302 .increment('login_count', 1)
221303 .where({ id: 1 })
222304 .execute()
docs/usage.mdmodified+3-3
Changes to docs/usage.md
@@ -136,7 +136,7 @@ await db
136136// Conditional delete (Chris's data cleanup)
137137await db
138138 .deleteFrom('sessions')
139 .where(['last_activity', '<', new Date(Date.now() - 30 _ 24 _ 60 _ 60 _ 1000)])
139 .where(['last_activity', '<', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)])
140140 .execute()
141141```
142142
@@ -381,7 +381,7 @@ query-builder file ./scripts/analytics.sql --output results.json
381381
382382```bash
383383# Performance analysis for Chris's dashboard queries
384query-builder explain "SELECT _ FROM users JOIN teams ON users.team_id = teams.id WHERE users.active = true"
384query-builder explain "SELECT * FROM users JOIN teams ON users.team_id = teams.id WHERE users.active = true"
385385
386386# Avery's e-commerce query optimization
387387query-builder explain "SELECT p._, c.name as category_name FROM products p JOIN categories c ON p.category_id = c.id WHERE p.featured = true"
@@ -394,7 +394,7 @@ query-builder explain --analyze "SELECT u.name, COUNT(p.id) as post_count FROM u
394394
395395```bash
396396# Parameterized queries (use with caution)
397query-builder unsafe "SELECT _ FROM users WHERE id = $1 AND team_id = $2" --params "[1, 3]"
397query-builder unsafe "SELECT * FROM users WHERE id = $1 AND team_id = $2" --params "[1, 3]"
398398
399399# Bulk operations with transaction support
400400query-builder file ./scripts/bulk_update.sql --transaction --batch-size 1000
packages/bun-query-builder/README.mdmodified+1-1
Changes to packages/bun-query-builder/README.md
@@ -33,7 +33,7 @@ await QueryBuilder
3333
3434// Update
3535await QueryBuilder
36 .update('users')
36 .updateTable('users')
3737 .set({ active: false })
3838 .where('id', '=', 1)
3939 .execute()