also looking at this
docs: the write-path pages documented an API that does not exist
#1066
20 files
+663
-240
| @@ -100,7 +100,7 @@ async function processOrder(orderData: { | ||
| 100 | 100 | const order = await tx.insertInto('orders').values({ |
| 101 | 101 | customer_id: orderData.customerId, |
| 102 | 102 | 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), | |
| 104 | 104 | created_at: new Date() |
| 105 | 105 | }).returning('id').execute() |
| 106 | 106 | |
| @@ -123,7 +123,7 @@ async function processOrder(orderData: { | ||
| 123 | 123 | // Stage 2: Process payment |
| 124 | 124 | const paymentResult = await processPayment({ |
| 125 | 125 | 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), | |
| 127 | 127 | orderId |
| 128 | 128 | }) |
| 129 | 129 | |
| @@ -506,7 +506,7 @@ async function monitoredOnboarding(userData: any) { | ||
| 506 | 506 | // Cleanup job for stale transactions |
| 507 | 507 | class TransactionCleanupJob { |
| 508 | 508 | async cleanupStaleTransactions() { |
| 509 | const staleThreshold = Date.now() - (24 _ 60 _ 60 _ 1000) // 24 hours | |
| 509 | const staleThreshold = Date.now() - (24 * 60 * 60 * 1000) // 24 hours | |
| 510 | 510 | |
| 511 | 511 | const staleTransactions = await db |
| 512 | 512 | .selectFrom('distributed_transactions') |
| @@ -551,7 +551,7 @@ class TransactionCleanupJob { | ||
| 551 | 551 | setInterval(async () => { |
| 552 | 552 | const cleanup = new TransactionCleanupJob() |
| 553 | 553 | await cleanup.cleanupStaleTransactions() |
| 554 | }, 60 _ 60 * 1000) // Every hour | |
| 554 | }, 60 * 60 * 1000) // Every hour | |
| 555 | 555 | ``` |
| 556 | 556 | |
| 557 | 557 | ## FAQ |
| @@ -64,7 +64,7 @@ const meta = buildSchemaMeta(models) | ||
| 64 | 64 | | `whereIn(col, vals)` | WHERE IN | `.whereIn('id', [1, 2, 3])` | |
| 65 | 65 | | `whereNotIn(col, vals)` | WHERE NOT IN | `.whereNotIn('status', ['banned'])` | |
| 66 | 66 | | `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')` | | |
| 68 | 68 | | `whereNotNull(col)` | WHERE IS NOT NULL | `.whereNotNull('email')` | |
| 69 | 69 | | `whereColumn(a, op, b)` | Compare columns | `.whereColumn('a', '=', 'b')` | |
| 70 | 70 | | `whereRaw(sql)` | Raw WHERE clause | `.whereRaw('LOWER(name) = ?', ['john'])` | |
| @@ -73,7 +73,7 @@ const meta = buildSchemaMeta(models) | ||
| 73 | 73 | |
| 74 | 74 | | Method | Description | Example | |
| 75 | 75 | |--------|-------------|---------| |
| 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')` | | |
| 77 | 77 | | `leftJoin(...)` | LEFT JOIN | `.leftJoin('posts', ...)` | |
| 78 | 78 | | `rightJoin(...)` | RIGHT JOIN | `.rightJoin('posts', ...)` | |
| 79 | 79 | | `crossJoin(table)` | CROSS JOIN | `.crossJoin('categories')` | |
| @@ -83,8 +83,8 @@ const meta = buildSchemaMeta(models) | ||
| 83 | 83 | | Method | Description | Example | |
| 84 | 84 | |--------|-------------|---------| |
| 85 | 85 | | `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()` | | |
| 88 | 88 | | `oldest()` | Order by created_at ASC | `.oldest()` | |
| 89 | 89 | | `inRandomOrder()` | Random order | `.inRandomOrder()` | |
| 90 | 90 | | `limit(n)` | LIMIT | `.limit(10)` | |
| @@ -127,7 +127,7 @@ const meta = buildSchemaMeta(models) | ||
| 127 | 127 | |
| 128 | 128 | | Method | Description | Example | |
| 129 | 129 | |--------|-------------|---------| |
| 130 | | `update(table)` | Start UPDATE | `db.update('users')` | | |
| 130 | | `update(table)` | Start UPDATE | `db.updateTable('users')` | | |
| 131 | 131 | | `set(data)` | Set values | `.set({ active: false })` | |
| 132 | 132 | | `increment(col, n)` | Increment | `.increment('views', 1)` | |
| 133 | 133 | | `decrement(col, n)` | Decrement | `.decrement('stock', 1)` | |
| @@ -196,7 +196,7 @@ Execute queries in a transaction. | ||
| 196 | 196 | ```typescript |
| 197 | 197 | await db.transaction(async (trx) => { |
| 198 | 198 | await trx.insertInto('users').values({...}).execute() |
| 199 | await trx.update('accounts').set({...}).execute() | |
| 199 | await trx.updateTable('accounts').set({...}).execute() | |
| 200 | 200 | }) |
| 201 | 201 | ``` |
| 202 | 202 | |
| @@ -2,6 +2,67 @@ | ||
| 2 | 2 | title: Delete Queries |
| 3 | 3 | description: Delete records from your database with type-safe queries. |
| 4 | 4 | --- |
| 5 | ||
| 6 | # Delete Queries | |
| 7 | ||
| 8 | Delete records with type-safe queries, soft deletes, and cascade support. | |
| 9 | ||
| 10 | ## Basic Delete | |
| 11 | ||
| 12 | ```typescript | |
| 13 | import { createQueryBuilder } from 'bun-query-builder' | |
| 14 | ||
| 15 | const db = createQueryBuilder<typeof schema>({ schema, meta }) | |
| 16 | ||
| 17 | // Delete by ID | |
| 18 | await db.remove('users', 1) | |
| 19 | ||
| 20 | // Delete with where clause | |
| 21 | await db | |
| 22 | .deleteFrom('users') | |
| 23 | .where({ active: false }) | |
| 24 | .execute() | |
| 25 | ``` | |
| 26 | ||
| 27 | ## Delete with Conditions | |
| 28 | ||
| 29 | ```typescript | |
| 30 | // Delete records matching conditions | |
| 31 | await db | |
| 32 | .deleteFrom('posts') | |
| 33 | .where('created_at', '<', '2023-01-01') | |
| 34 | .execute() | |
| 35 | ||
| 36 | // Delete with multiple conditions | |
| 37 | await 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 | |
| 48 | await db.deleteMany('users', [1, 2, 3, 4, 5]) | |
| 49 | ||
| 50 | // Delete many with conditions | |
| 51 | await db | |
| 52 | .deleteFrom('logs') | |
| 53 | .where('level', '=', 'debug') | |
| 54 | .where('created_at', '<', '2024-01-01') | |
| 55 | .execute() | |
| 56 | ``` | |
| 57 | ||
| 58 | ## Soft Deletes | |
| 59 | ||
| 60 | If your model supports soft deletes, records are marked as deleted instead of being removed: | |
| 61 | ||
| 62 | ```typescript | |
| 63 | // Model with soft deletes enabled | |
| 64 | const models = { | |
| 65 | User: { | |
| 5 | 66 | name: 'User', |
| 6 | 67 | table: 'users', |
| 7 | 68 | softDeletes: true, // Enable soft deletes |
| @@ -10,7 +71,7 @@ description: Delete records from your database with type-safe queries. | ||
| 10 | 71 | } |
| 11 | 72 | |
| 12 | 73 | // Soft delete a record |
| 13 | await db.delete('users', 1) | |
| 74 | await db.remove('users', 1) | |
| 14 | 75 | // Sets deleted_at = NOW() instead of removing the record |
| 15 | 76 | |
| 16 | 77 | // Query excluding soft deleted records (default behavior) |
| @@ -34,13 +95,15 @@ const deletedUsers = await db | ||
| 34 | 95 | |
| 35 | 96 | ```typescript |
| 36 | 97 | |
| 37 | // Restore a soft deleted record | |
| 38 | await db.restore('users', 1) | |
| 98 | // Restoring is a model-layer operation: load the trashed row, then restore it. | |
| 99 | const user = await User.withTrashed().find(1) | |
| 100 | await user.restore() | |
| 39 | 101 | // Sets deleted_at = NULL |
| 40 | 102 | |
| 41 | // Restore with conditions | |
| 103 | // From the query builder, clear the soft-delete column directly | |
| 42 | 104 | await db |
| 43 | .restoreFrom('users') | |
| 105 | .updateTable('users') | |
| 106 | .set({ deleted_at: null }) | |
| 44 | 107 | .where({ email: 'restored@example.com' }) |
| 45 | 108 | .execute() |
| 46 | 109 | |
| @@ -50,8 +113,9 @@ await db | ||
| 50 | 113 | |
| 51 | 114 | ```typescript |
| 52 | 115 | |
| 53 | // Permanently delete a soft deleted record | |
| 54 | await 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. | |
| 118 | await db.remove('users', 1) | |
| 55 | 119 | |
| 56 | 120 | // Force delete with conditions |
| 57 | 121 | await db |
| @@ -106,11 +170,15 @@ Remove all records from a table: | ||
| 106 | 170 | |
| 107 | 171 | ```typescript |
| 108 | 172 | |
| 109 | // Truncate entire table | |
| 110 | await db.truncate('logs') | |
| 173 | // Truncate entire table — a model-layer static | |
| 174 | await Log.truncate() | |
| 175 | ||
| 176 | // From the query builder, issue the statement directly. | |
| 177 | // TRUNCATE is not supported by SQLite; use DELETE there. | |
| 178 | await db.unsafe('TRUNCATE TABLE logs') | |
| 111 | 179 | |
| 112 | 180 | // With cascade (if foreign keys exist) |
| 113 | await db.truncate('users', { cascade: true }) | |
| 181 | await db.unsafe('TRUNCATE TABLE users CASCADE') | |
| 114 | 182 | |
| 115 | 183 | ``` |
| 116 | 184 | |
| @@ -126,7 +194,7 @@ await db.transaction(async (trx) => { | ||
| 126 | 194 | .execute() |
| 127 | 195 | |
| 128 | 196 | // Then delete the user |
| 129 | await trx.delete('users', userId) | |
| 197 | await trx.remove('users', userId) | |
| 130 | 198 | |
| 131 | 199 | // All or nothing - if any delete fails, all are rolled back |
| 132 | 200 | }) |
| @@ -149,7 +217,7 @@ async function deleteUserWithRelations(userId: number) { | ||
| 149 | 217 | await trx.deleteFrom('posts').where({ user_id: userId }).execute() |
| 150 | 218 | |
| 151 | 219 | // Delete user |
| 152 | await trx.delete('users', userId) | |
| 220 | await trx.remove('users', userId) | |
| 153 | 221 | }) |
| 154 | 222 | } |
| 155 | 223 | |
| @@ -225,15 +293,15 @@ async function cleanupData() { | ||
| 225 | 293 | .where('created_at', '<', '2023-01-01') |
| 226 | 294 | .execute() |
| 227 | 295 | |
| 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() | |
| 230 | 298 | |
| 231 | 299 | // Query with trashed |
| 232 | 300 | const allUsers = await db.selectFrom('users').withTrashed().get() |
| 233 | 301 | const deletedOnly = await db.selectFrom('users').onlyTrashed().get() |
| 234 | 302 | |
| 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]) | |
| 237 | 305 | |
| 238 | 306 | console.log('Cleanup completed') |
| 239 | 307 | } |
| @@ -707,7 +707,7 @@ await db | ||
| 707 | 707 | // Delete with complex conditions |
| 708 | 708 | await db |
| 709 | 709 | .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 | |
| 711 | 711 | .andWhere(['user_id', 'is not', null]) |
| 712 | 712 | .execute() |
| 713 | 713 | |
| @@ -765,7 +765,7 @@ See the Pagination page for details. Highlights: | ||
| 765 | 765 | await db.selectFrom('users').paginate(25, 1) |
| 766 | 766 | await db.selectFrom('users').simplePaginate(25) |
| 767 | 767 | await db.selectFrom('users').cursorPaginate(100, undefined, 'id', 'asc') |
| 768 | await db.selectFrom('users').chunkById(1000, 'id', async (batch) => { /_ ... */ }) | |
| 768 | await db.selectFrom('users').chunkById(1000, 'id', async (batch) => { /* ... */ }) | |
| 769 | 769 | ``` |
| 770 | 770 | |
| 771 | 771 | ## CTEs and Recursive Queries |
| @@ -1139,7 +1139,7 @@ const projectsWithCollaborators = await db | ||
| 1139 | 1139 | ```ts |
| 1140 | 1140 | // Cleanup old sessions |
| 1141 | 1141 | async 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 | |
| 1143 | 1143 | |
| 1144 | 1144 | const deletedCount = await db |
| 1145 | 1145 | .deleteFrom('user_sessions') |
| @@ -1159,7 +1159,7 @@ async function archiveCompletedProjects() { | ||
| 1159 | 1159 | .selectFrom('projects') |
| 1160 | 1160 | .select('projects', 'id', 'name') |
| 1161 | 1161 | .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)]) | |
| 1163 | 1163 | .execute() |
| 1164 | 1164 | |
| 1165 | 1165 | if (projectsToArchive.length === 0) |
| @@ -725,8 +725,8 @@ query-builder file ./migrations/seed.sql | ||
| 725 | 725 | query-builder file ./scripts/cleanup.sql --params "[30]" # 30 days retention |
| 726 | 726 | |
| 727 | 727 | # Execute parameterized queries (use with caution) |
| 728 | query-builder unsafe "SELECT _ FROM users WHERE team = $1" --params "[\"Engineering\"]" | |
| 729 | query-builder unsafe "SELECT _ FROM projects WHERE owner = $1 AND status = $2" --params "[\"Chris\",\"active\"]" | |
| 728 | query-builder unsafe "SELECT * FROM users WHERE team = $1" --params "[\"Engineering\"]" | |
| 729 | query-builder unsafe "SELECT * FROM projects WHERE owner = $1 AND status = $2" --params "[\"Chris\",\"active\"]" | |
| 730 | 730 | ``` |
| 731 | 731 | |
| 732 | 732 | ### Performance Analysis |
| @@ -738,12 +738,12 @@ query-builder benchmark --iterations 5000 | ||
| 738 | 738 | query-builder benchmark --operations select,insert,count |
| 739 | 739 | |
| 740 | 740 | # Analyze query performance |
| 741 | query-builder explain "SELECT _ FROM users WHERE active = true" | |
| 741 | query-builder explain "SELECT * FROM users WHERE active = true" | |
| 742 | 742 | query-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'" |
| 743 | 743 | |
| 744 | 744 | # Compare different query strategies |
| 745 | query-builder explain "SELECT _ FROM users WHERE name LIKE '%Chris%'" | |
| 746 | query-builder explain "SELECT _ FROM users WHERE name ILIKE 'chris%'" # PostgreSQL | |
| 745 | query-builder explain "SELECT * FROM users WHERE name LIKE '%Chris%'" | |
| 746 | query-builder explain "SELECT * FROM users WHERE name ILIKE 'chris%'" # PostgreSQL | |
| 747 | 747 | ``` |
| 748 | 748 | |
| 749 | 749 | ## Best Practices |
| @@ -778,7 +778,7 @@ query-builder introspect ./app/Models --json | jq '.tables | keys' | ||
| 778 | 778 | query-builder sql ./app/Models users --where '{"team":"Chris Team"}' --limit 5 |
| 779 | 779 | |
| 780 | 780 | # Good: Check performance impact |
| 781 | query-builder explain "SELECT _ FROM users u JOIN projects p ON p.user_id = u.id WHERE u.active = true" | |
| 781 | query-builder explain "SELECT * FROM users u JOIN projects p ON p.user_id = u.id WHERE u.active = true" | |
| 782 | 782 | query-builder benchmark --operations select,join |
| 783 | 783 | |
| 784 | 784 | # Good: Manage cache during development |
| @@ -786,7 +786,7 @@ query-builder cache:clear | ||
| 786 | 786 | query-builder cache:stats |
| 787 | 787 | |
| 788 | 788 | # Avoid: Direct SQL without parameterization |
| 789 | query-builder unsafe "SELECT _ FROM users WHERE name = 'Chris'" # Risky! | |
| 789 | query-builder unsafe "SELECT * FROM users WHERE name = 'Chris'" # Risky! | |
| 790 | 790 | |
| 791 | 791 | # Better: Use parameters |
| 792 | 792 | query-builder unsafe "SELECT * FROM users WHERE name = $1" --params '["Chris"]' |
| @@ -882,7 +882,7 @@ if [[ ! "$USER_ID" =~ ^[0-9]+$ ]]; then | ||
| 882 | 882 | echo "Invalid user ID" |
| 883 | 883 | exit 1 |
| 884 | 884 | fi |
| 885 | query-builder unsafe "SELECT _ FROM users WHERE id = $1" --params "[\"$USER_ID\"]" | |
| 885 | query-builder unsafe "SELECT * FROM users WHERE id = $1" --params "[\"$USER_ID\"]" | |
| 886 | 886 | |
| 887 | 887 | # Production audit logging |
| 888 | 888 | echo "$(date): CLI operation by $(whoami): $_" >> /var/log/query-builder.log |
| @@ -904,7 +904,7 @@ query-builder ping || echo "Database unreachable - check network/credentials" | ||
| 904 | 904 | |
| 905 | 905 | # Debugging performance issues |
| 906 | 906 | query-builder benchmark --iterations 100 |
| 907 | query-builder explain "SELECT _ FROM large_table WHERE unindexed_column = 'value'" | |
| 907 | query-builder explain "SELECT * FROM large_table WHERE unindexed_column = 'value'" | |
| 908 | 908 | |
| 909 | 909 | # Debugging schema mismatches |
| 910 | 910 | query-builder validate:schema ./app/Models |
| @@ -974,5 +974,5 @@ query-builder file ./migrations/seed.sql --params "[\"role\", \"admin\"]" | ||
| 974 | 974 | ### Explain a complex statement |
| 975 | 975 | |
| 976 | 976 | ```bash |
| 977 | query-builder explain "SELECT _ FROM users WHERE active = true ORDER BY created_at DESC LIMIT 10" | |
| 977 | query-builder explain "SELECT * FROM users WHERE active = true ORDER BY created_at DESC LIMIT 10" | |
| 978 | 978 | ``` |
| @@ -64,12 +64,12 @@ Implementation details: | ||
| 64 | 64 | |
| 65 | 65 | ### Composite cursors |
| 66 | 66 | |
| 67 | You can pass multiple columns for deterministic ordering, e.g. `['created*at', 'id']`. | |
| 67 | You can pass multiple columns for deterministic ordering, e.g. `['created_at', 'id']`. | |
| 68 | 68 | |
| 69 | 69 | ```ts |
| 70 | 70 | const page = await db |
| 71 | 71 | .selectFrom('users') |
| 72 | .cursorPaginate(50, undefined, ['created*at', 'id'], 'asc') | |
| 72 | .cursorPaginate(50, undefined, ['created_at', 'id'], 'asc') | |
| 73 | 73 | |
| 74 | 74 | // use page.meta.nextCursor to fetch the next window |
| 75 | 75 | ``` |
| @@ -126,9 +126,9 @@ await db.selectFrom('users').eachById(500, 'id', async (row) => { | ||
| 126 | 126 | async function getUsersPage(page: number = 1, perPage: number = 20) { |
| 127 | 127 | const { data, meta } = await db |
| 128 | 128 | .selectFrom('users') |
| 129 | .select('users', 'id', 'name', 'email', 'role', 'created*at') | |
| 129 | .select('users', 'id', 'name', 'email', 'role', 'created_at') | |
| 130 | 130 | .where({ active: true }) |
| 131 | .orderBy('created*at', 'desc') | |
| 131 | .orderBy('created_at', 'desc') | |
| 132 | 132 | .paginate(perPage, page) |
| 133 | 133 | |
| 134 | 134 | return { |
| @@ -148,7 +148,7 @@ async function getUsersPage(page: number = 1, perPage: number = 20) { | ||
| 148 | 148 | const chrisProjects = await db |
| 149 | 149 | .selectFrom('projects') |
| 150 | 150 | .where({ owner: 'Chris' }) |
| 151 | .orderBy('updated*at', 'desc') | |
| 151 | .orderBy('updated_at', 'desc') | |
| 152 | 152 | .paginate(10, 1) |
| 153 | 153 | ``` |
| 154 | 154 | |
| @@ -159,10 +159,10 @@ const chrisProjects = await db | ||
| 159 | 159 | async function getInfinitePosts(cursor?: string) { |
| 160 | 160 | const page = await db |
| 161 | 161 | .selectFrom('posts') |
| 162 | .select('posts', 'id', 'title', 'content', 'author*id', 'created*at') | |
| 162 | .select('posts', 'id', 'title', 'content', 'author_id', 'created_at') | |
| 163 | 163 | .with('Author') |
| 164 | 164 | .where({ published: true }) |
| 165 | .cursorPaginate(20, cursor, 'created*at', 'desc') | |
| 165 | .cursorPaginate(20, cursor, 'created_at', 'desc') | |
| 166 | 166 | |
| 167 | 167 | return { |
| 168 | 168 | posts: page.data, |
| @@ -200,7 +200,7 @@ async function sendNewsletterToAllUsers() { | ||
| 200 | 200 | |
| 201 | 201 | await db |
| 202 | 202 | .selectFrom('users') |
| 203 | .where({ email*verified: true, newsletter*subscribed: true }) | |
| 203 | .where({ email_verified: true, newsletter_subscribed: true }) | |
| 204 | 204 | .chunkById(100, 'id', async (userBatch) => { |
| 205 | 205 | // Process batch of users |
| 206 | 206 | for (const user of userBatch) { |
| @@ -220,15 +220,15 @@ async function exportUserData() { | ||
| 220 | 220 | let exportedCount = 0 |
| 221 | 221 | |
| 222 | 222 | // Write CSV header |
| 223 | exportFile.write('id,name,email,created*at\n') | |
| 223 | exportFile.write('id,name,email,created_at\n') | |
| 224 | 224 | |
| 225 | 225 | await db |
| 226 | 226 | .selectFrom('users') |
| 227 | .select('users', 'id', 'name', 'email', 'created*at') | |
| 227 | .select('users', 'id', 'name', 'email', 'created_at') | |
| 228 | 228 | .orderBy('id', 'asc') |
| 229 | 229 | .chunk(500, async (userBatch) => { |
| 230 | 230 | 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`) | |
| 232 | 232 | exportedCount++ |
| 233 | 233 | } |
| 234 | 234 | |
| @@ -265,16 +265,16 @@ async function searchUsersWithPagination(query: string, page: number = 1) { | ||
| 265 | 265 | // Time-based cursor pagination for real-time feeds |
| 266 | 266 | async function getRealtimeFeed(cursor?: string, userId?: number) { |
| 267 | 267 | 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') | |
| 270 | 270 | .with('User') |
| 271 | .orderBy('created*at', 'desc') | |
| 271 | .orderBy('created_at', 'desc') | |
| 272 | 272 | |
| 273 | 273 | if (userId) { |
| 274 | query = query.where({ user*id: userId }) | |
| 274 | query = query.where({ user_id: userId }) | |
| 275 | 275 | } |
| 276 | 276 | |
| 277 | const page = await query.cursorPaginate(25, cursor, 'created*at', 'desc') | |
| 277 | const page = await query.cursorPaginate(25, cursor, 'created_at', 'desc') | |
| 278 | 278 | |
| 279 | 279 | return { |
| 280 | 280 | items: page.data, |
| @@ -308,12 +308,12 @@ const buddyFeed = await getRealtimeFeed(undefined, buddyUserId) | ||
| 308 | 308 | const page = await db |
| 309 | 309 | .selectFrom('posts') |
| 310 | 310 | .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 | |
| 312 | 312 | |
| 313 | 313 | // Good: Composite cursor for stable ordering |
| 314 | 314 | const page = await db |
| 315 | 315 | .selectFrom('events') |
| 316 | .cursorPaginate(25, cursor, ['created*at', 'id'], 'desc') // Both columns indexed | |
| 316 | .cursorPaginate(25, cursor, ['created_at', 'id'], 'desc') // Both columns indexed | |
| 317 | 317 | |
| 318 | 318 | // Avoid: Large offset pagination |
| 319 | 319 | const badPage = await db |
| @@ -334,16 +334,16 @@ async function getLatestNotifications(userId: number, cursor?: string) { | ||
| 334 | 334 | // Cursor pagination is stable even if new notifications are added |
| 335 | 335 | return await db |
| 336 | 336 | .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') | |
| 339 | 339 | } |
| 340 | 340 | |
| 341 | 341 | // Composite cursor for handling duplicate timestamps |
| 342 | 342 | async function getOrderHistory(customerId: number, cursor?: string) { |
| 343 | 343 | return await db |
| 344 | 344 | .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') | |
| 347 | 347 | } |
| 348 | 348 | ``` |
| 349 | 349 | |
| @@ -374,12 +374,12 @@ interface PaginatedResponse<T> { | ||
| 374 | 374 | async function getUsersAPI(req: Request): Promise<PaginatedResponse<User>> { |
| 375 | 375 | try { |
| 376 | 376 | 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)) | |
| 378 | 378 | |
| 379 | 379 | const result = await db |
| 380 | 380 | .selectFrom('users') |
| 381 | 381 | .where({ active: true }) |
| 382 | .orderBy('created*at', 'desc') | |
| 382 | .orderBy('created_at', 'desc') | |
| 383 | 383 | .paginate(perPage, page) |
| 384 | 384 | |
| 385 | 385 | return { |
| @@ -415,7 +415,7 @@ async function processLargeDataset(batchSize: number = 1000) { | ||
| 415 | 415 | |
| 416 | 416 | try { |
| 417 | 417 | await db |
| 418 | .selectFrom('large*table') | |
| 418 | .selectFrom('large_table') | |
| 419 | 419 | .where(['id', '>', lastProcessedId]) |
| 420 | 420 | .orderBy('id', 'asc') |
| 421 | 421 | .chunkById(batchSize, 'id', async (batch) => { |
| @@ -513,7 +513,7 @@ const UsersList = () => { | ||
| 513 | 513 | |
| 514 | 514 | ### Can I use composite cursors |
| 515 | 515 | |
| 516 | Yes. Provide an array of columns like `['created*at', 'id']`. The cursor will carry a tuple of values in order. | |
| 516 | Yes. Provide an array of columns like `['created_at', 'id']`. The cursor will carry a tuple of values in order. | |
| 517 | 517 | |
| 518 | 518 | ### How do I resume a chunked job |
| 519 | 519 | |
| @@ -530,7 +530,7 @@ let cursor: number | undefined | ||
| 530 | 530 | do { |
| 531 | 531 | const { data, meta } = await db |
| 532 | 532 | .selectFrom('events') |
| 533 | .cursorPaginate(100, cursor, 'created*at', 'desc') | |
| 533 | .cursorPaginate(100, cursor, 'created_at', 'desc') | |
| 534 | 534 | // process new slice |
| 535 | 535 | cursor = meta.nextCursor as number | undefined |
| 536 | 536 | } while (cursor) |
| @@ -551,15 +551,15 @@ const { data, meta } = await db | ||
| 551 | 551 | ```ts |
| 552 | 552 | await db |
| 553 | 553 | .selectFrom('posts') |
| 554 | .orderBy('published*at', 'desc') | |
| 554 | .orderBy('published_at', 'desc') | |
| 555 | 555 | .paginate(10, 1) |
| 556 | 556 | ``` |
| 557 | 557 | |
| 558 | 558 | ### Chunk with transformation |
| 559 | 559 | |
| 560 | 560 | ```ts |
| 561 | await db.selectFrom('audit*logs').chunk(5000, async (rows) => { | |
| 562 | const normalized = rows.map(r => ({ ...r, ts: new Date(r.created*at) })) | |
| 561 | await db.selectFrom('audit_logs').chunk(5000, async (rows) => { | |
| 562 | const normalized = rows.map(r => ({ ...r, ts: new Date(r.created_at) })) | |
| 563 | 563 | await writeLogs(normalized) |
| 564 | 564 | }) |
| 565 | 565 | ``` |
| @@ -577,5 +577,5 @@ await db.selectFrom('images').eachById(200, 'id', async (row) => { | ||
| 577 | 577 | Ensure the cursor column has an index; otherwise, performance degrades. |
| 578 | 578 | |
| 579 | 579 | ```sql |
| 580 | CREATE INDEX IF NOT EXISTS idx*users*id ON users(id); | |
| 580 | CREATE INDEX IF NOT EXISTS idx_users_id ON users(id); | |
| 581 | 581 | ``` |
| @@ -357,7 +357,7 @@ const buddyManagers = await db | ||
| 357 | 357 | const averyActiveTeam = await db |
| 358 | 358 | .selectFrom('users') |
| 359 | 359 | .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)]) | |
| 361 | 361 | .execute() |
| 362 | 362 | ``` |
| 363 | 363 | |
| @@ -47,8 +47,8 @@ export default class UserSeeder extends Seeder { | ||
| 47 | 47 | const users = Array.from({ length: 10 }, () => ({ |
| 48 | 48 | name: faker.person.fullName(), |
| 49 | 49 | email: faker.internet.email(), |
| 50 | created*at: new Date(), | |
| 51 | updated*at: new Date(), | |
| 50 | created_at: new Date(), | |
| 51 | updated_at: new Date(), | |
| 52 | 52 | })) |
| 53 | 53 | |
| 54 | 54 | await qb.table('users').insert(users).execute() |
| @@ -99,8 +99,8 @@ export default class ProductSeeder extends Seeder { | ||
| 99 | 99 | description: faker.commerce.productDescription(), |
| 100 | 100 | price: faker.commerce.price(10, 1000), |
| 101 | 101 | 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(), | |
| 104 | 104 | })) |
| 105 | 105 | |
| 106 | 106 | await qb.table('products').insert(products).execute() |
| @@ -120,8 +120,8 @@ export default class UserSeeder extends Seeder { | ||
| 120 | 120 | name: faker.person.fullName(), |
| 121 | 121 | email: faker.internet.email(), |
| 122 | 122 | 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(), | |
| 125 | 125 | })) |
| 126 | 126 | |
| 127 | 127 | await qb.table('users').insert(users).execute() |
| @@ -148,12 +148,12 @@ export default class PostSeeder extends Seeder { | ||
| 148 | 148 | for (const user of users) { |
| 149 | 149 | for (let i = 0; i < 3; i++) { |
| 150 | 150 | posts.push({ |
| 151 | user*id: user.id, | |
| 151 | user_id: user.id, | |
| 152 | 152 | title: faker.lorem.sentence(5), |
| 153 | 153 | body: faker.lorem.paragraphs(3), |
| 154 | 154 | 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(), | |
| 157 | 157 | }) |
| 158 | 158 | } |
| 159 | 159 | } |
| @@ -181,8 +181,8 @@ export default class LargeDataSeeder extends Seeder { | ||
| 181 | 181 | const batch = Array.from({ length: batchSize }, () => ({ |
| 182 | 182 | name: faker.person.fullName(), |
| 183 | 183 | email: faker.internet.email(), |
| 184 | created*at: new Date(), | |
| 185 | updated*at: new Date(), | |
| 184 | created_at: new Date(), | |
| 185 | updated_at: new Date(), | |
| 186 | 186 | })) |
| 187 | 187 | |
| 188 | 188 | await qb.table('users').insert(batch).execute() |
| @@ -211,8 +211,8 @@ export default class ConditionalSeeder extends Seeder { | ||
| 211 | 211 | const users = Array.from({ length: 10 }, () => ({ |
| 212 | 212 | name: faker.person.fullName(), |
| 213 | 213 | email: faker.internet.email(), |
| 214 | created*at: new Date(), | |
| 215 | updated*at: new Date(), | |
| 214 | created_at: new Date(), | |
| 215 | updated_at: new Date(), | |
| 216 | 216 | })) |
| 217 | 217 | |
| 218 | 218 | await qb.table('users').insert(users).execute() |
| @@ -237,7 +237,7 @@ faker.person.jobTitle() // "Software Engineer" | ||
| 237 | 237 | |
| 238 | 238 | ```typescript |
| 239 | 239 | faker.internet.email() // "john.doe@example.com" |
| 240 | faker.internet.userName() // "john*doe123" | |
| 240 | faker.internet.userName() // "john_doe123" | |
| 241 | 241 | faker.internet.url() // "https://example.com" |
| 242 | 242 | faker.internet.password() // "aB3$dF7!" |
| 243 | 243 | ``` |
| @@ -438,7 +438,7 @@ Use appropriate faker methods for each field: | ||
| 438 | 438 | phone: faker.phone.number(), // Valid phone number |
| 439 | 439 | age: faker.number.int(18, 80), // Reasonable age range |
| 440 | 440 | 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 | |
| 442 | 442 | } |
| 443 | 443 | ``` |
| 444 | 444 | |
| @@ -505,8 +505,8 @@ function createUser(overrides = {}) { | ||
| 505 | 505 | email: faker.internet.email(), |
| 506 | 506 | age: faker.number.int(18, 80), |
| 507 | 507 | role: 'user', |
| 508 | created*at: new Date(), | |
| 509 | updated*at: new Date(), | |
| 508 | created_at: new Date(), | |
| 509 | updated_at: new Date(), | |
| 510 | 510 | ...overrides, |
| 511 | 511 | } |
| 512 | 512 | } |
| @@ -537,12 +537,12 @@ export default class UserRoleSeeder extends Seeder { | ||
| 537 | 537 | const userRoles = users.flatMap(user => |
| 538 | 538 | faker.helpers.arrayElements(roles, faker.number.int(1, 3)) |
| 539 | 539 | .map(role => ({ |
| 540 | user*id: user.id, | |
| 541 | role*id: role.id, | |
| 540 | user_id: user.id, | |
| 541 | role_id: role.id, | |
| 542 | 542 | })) |
| 543 | 543 | ) |
| 544 | 544 | |
| 545 | await qb.table('user*roles').insert(userRoles).execute() | |
| 545 | await qb.table('user_roles').insert(userRoles).execute() | |
| 546 | 546 | } |
| 547 | 547 | |
| 548 | 548 | get order(): number { |
| @@ -556,7 +556,7 @@ export default class UserRoleSeeder extends Seeder { | ||
| 556 | 556 | ```typescript |
| 557 | 557 | export default class EnvironmentSeeder extends Seeder { |
| 558 | 558 | async run(qb: any): Promise<void> { |
| 559 | const env = process.env.NODE*ENV | |
| 559 | const env = process.env.NODE_ENV | |
| 560 | 560 | |
| 561 | 561 | if (env === 'production') { |
| 562 | 562 | console.log('Skipping seeding in production') |
| @@ -160,7 +160,7 @@ await createUser({ name: 'Alice' }) | ||
| 160 | 160 | You can pass options to override defaults: |
| 161 | 161 | |
| 162 | 162 | ```ts |
| 163 | const createWithRetry = db.transactional(async (tx) => { /_ ... _/ }, { retries: 3 }) | |
| 163 | const createWithRetry = db.transactional(async (tx) => { /* ... */ }, { retries: 3 }) | |
| 164 | 164 | ``` |
| 165 | 165 | |
| 166 | 166 | ## Error Handling and Logging |
| @@ -168,7 +168,7 @@ const createWithRetry = db.transactional(async (tx) => { /_ ... _/ }, { retries: | ||
| 168 | 168 | Use `onRetry` and `logger` to observe transaction lifecycle. |
| 169 | 169 | |
| 170 | 170 | ```ts |
| 171 | await db.transaction(async (tx) => { /_ ... _/ }, { | |
| 171 | await db.transaction(async (tx) => { /* ... */ }, { | |
| 172 | 172 | logger: (e) => { |
| 173 | 173 | if (e.type === 'retry') |
| 174 | 174 | console.warn('retry', e.attempt) |
| @@ -86,7 +86,7 @@ await db | ||
| 86 | 86 | |
| 87 | 87 | ```typescript |
| 88 | 88 | await db |
| 89 | .update('users') | |
| 89 | .updateTable('users') | |
| 90 | 90 | .set({ active: false }) |
| 91 | 91 | .where({ id: 1 }) |
| 92 | 92 | .execute() |
| @@ -96,7 +96,7 @@ await db | ||
| 96 | 96 | |
| 97 | 97 | ```typescript |
| 98 | 98 | await db |
| 99 | .update('users') | |
| 99 | .updateTable('users') | |
| 100 | 100 | .set({ |
| 101 | 101 | name: 'Updated Name', |
| 102 | 102 | email: 'new@example.com', |
| @@ -111,7 +111,7 @@ await db | ||
| 111 | 111 | ```typescript |
| 112 | 112 | // Increment a value |
| 113 | 113 | await db |
| 114 | .update('products') | |
| 114 | .updateTable('products') | |
| 115 | 115 | .set({ |
| 116 | 116 | views: db.raw('views + 1') |
| 117 | 117 | }) |
| @@ -120,7 +120,7 @@ await db | ||
| 120 | 120 | |
| 121 | 121 | // Decrement stock |
| 122 | 122 | await db |
| 123 | .update('products') | |
| 123 | .updateTable('products') | |
| 124 | 124 | .set({ |
| 125 | 125 | stock: db.raw('stock - ?', [quantity]) |
| 126 | 126 | }) |
| @@ -144,13 +144,9 @@ await db.updateMany( | ||
| 144 | 144 | |
| 145 | 145 | ```typescript |
| 146 | 146 | await db |
| 147 | .update('products') | |
| 147 | .updateTable('products') | |
| 148 | 148 | .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']), | |
| 154 | 150 | }) |
| 155 | 151 | .where({ type: 'gadget' }) |
| 156 | 152 | .execute() |
| @@ -191,8 +187,9 @@ await db.deleteMany('users', [1, 2, 3, 4, 5]) | ||
| 191 | 187 | // Delete all records (use with caution!) |
| 192 | 188 | await db.deleteFrom('temp_data').execute() |
| 193 | 189 | |
| 194 | // Truncate table (faster, resets auto-increment) | |
| 195 | await db.truncate('temp_data') | |
| 190 | // Truncate table (faster, resets auto-increment). | |
| 191 | // Not supported by SQLite — use the DELETE above there. | |
| 192 | await db.unsafe('TRUNCATE TABLE temp_data') | |
| 196 | 193 | ``` |
| 197 | 194 | |
| 198 | 195 | ## Soft Deletes |
| @@ -202,7 +199,7 @@ await db.truncate('temp_data') | ||
| 202 | 199 | ```typescript |
| 203 | 200 | // Instead of deleting, set deleted_at |
| 204 | 201 | await db |
| 205 | .update('users') | |
| 202 | .updateTable('users') | |
| 206 | 203 | .set({ deleted_at: new Date() }) |
| 207 | 204 | .where({ id: 1 }) |
| 208 | 205 | .execute() |
| @@ -221,7 +218,7 @@ const activeUsers = await db | ||
| 221 | 218 | |
| 222 | 219 | ```typescript |
| 223 | 220 | await db |
| 224 | .update('users') | |
| 221 | .updateTable('users') | |
| 225 | 222 | .set({ deleted_at: null }) |
| 226 | 223 | .where({ id: 1 }) |
| 227 | 224 | .execute() |
| @@ -276,7 +273,7 @@ async function processOrder(orderId: number) { | ||
| 276 | 273 | await db.transaction(async (trx) => { |
| 277 | 274 | // Update order status |
| 278 | 275 | await trx |
| 279 | .update('orders') | |
| 276 | .updateTable('orders') | |
| 280 | 277 | .set({ |
| 281 | 278 | status: 'processing', |
| 282 | 279 | processed_at: new Date() |
| @@ -293,7 +290,7 @@ async function processOrder(orderId: number) { | ||
| 293 | 290 | // Decrement stock for each item |
| 294 | 291 | for (const item of items) { |
| 295 | 292 | await trx |
| 296 | .update('products') | |
| 293 | .updateTable('products') | |
| 297 | 294 | .set({ |
| 298 | 295 | stock: db.raw('stock - ?', [item.quantity]) |
| 299 | 296 | }) |
| @@ -322,7 +319,7 @@ async function deactivateInactiveUsers(daysInactive: number) { | ||
| 322 | 319 | cutoffDate.setDate(cutoffDate.getDate() - daysInactive) |
| 323 | 320 | |
| 324 | 321 | const result = await db |
| 325 | .update('users') | |
| 322 | .updateTable('users') | |
| 326 | 323 | .set({ |
| 327 | 324 | active: false, |
| 328 | 325 | deactivated_at: new Date(), |
| @@ -17,7 +17,7 @@ await transaction(async (trx) => { | ||
| 17 | 17 | .execute() |
| 18 | 18 | |
| 19 | 19 | await trx |
| 20 | .update('users') | |
| 20 | .updateTable('users') | |
| 21 | 21 | .set({ has_account: true }) |
| 22 | 22 | .where({ id: 1 }) |
| 23 | 23 | .execute() |
| @@ -77,20 +77,24 @@ await transaction(fn, { isolationLevel: 'serializable' }) | ||
| 77 | 77 | |
| 78 | 78 | Create savepoints within a transaction: |
| 79 | 79 | |
| 80 | `savepoint()` takes a callback, like `transaction()` does — there is no | |
| 81 | `rollbackToSavepoint()`. The savepoint is released when the callback returns and | |
| 82 | rolled back if it throws, so "roll back to the savepoint" means "let the | |
| 83 | callback throw and catch it outside". | |
| 84 | ||
| 80 | 85 | ```typescript |
| 81 | await transaction(async (trx) => { | |
| 86 | await db.transaction(async (trx) => { | |
| 82 | 87 | await trx.insertInto('logs').values({ message: 'Step 1' }).execute() |
| 83 | 88 | |
| 84 | // Create savepoint | |
| 85 | await trx.savepoint('step2') | |
| 86 | ||
| 87 | 89 | 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 | |
| 94 | 98 | await trx.insertInto('logs').values({ message: 'Step 2 fallback' }).execute() |
| 95 | 99 | } |
| 96 | 100 | |
| @@ -98,26 +102,41 @@ await transaction(async (trx) => { | ||
| 98 | 102 | }) |
| 99 | 103 | ``` |
| 100 | 104 | |
| 101 | ## Manual Transaction Control | |
| 105 | `savepoint()` must be called inside a transaction; calling it outside throws. | |
| 106 | ||
| 107 | ## Commit and rollback | |
| 102 | 108 | |
| 103 | For more control over transaction lifecycle: | |
| 109 | There is no manual `beginTransaction()` / `commit()` / `rollback()` API. The | |
| 110 | callback form owns the lifecycle: return normally and the transaction commits, | |
| 111 | throw and it rolls back. | |
| 104 | 112 | |
| 105 | 113 | ```typescript |
| 106 | const trx = await db.beginTransaction() | |
| 114 | await 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 | ``` | |
| 107 | 120 | |
| 108 | try { | |
| 109 | await trx.insertInto('users').values({ name: 'Test' }).execute() | |
| 110 | await trx.update('counters').set({ value: db.raw('value + 1') }).execute() | |
| 121 | To roll back deliberately, throw — then handle the error outside: | |
| 111 | 122 | |
| 112 | // Explicitly commit | |
| 113 | await trx.commit() | |
| 114 | } catch (error) { | |
| 115 | // Explicitly rollback | |
| 116 | await trx.rollback() | |
| 117 | throw error | |
| 123 | ```typescript | |
| 124 | try { | |
| 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 | } | |
| 131 | catch (error) { | |
| 132 | // the transaction has already rolled back by the time you get here | |
| 118 | 133 | } |
| 119 | 134 | ``` |
| 120 | 135 | |
| 136 | Use `tx`, not `db`, for every statement inside the callback. `db` holds a | |
| 137 | different connection, so work issued through it is not part of the transaction | |
| 138 | and will not be rolled back with it. | |
| 139 | ||
| 121 | 140 | ## Nested Transactions |
| 122 | 141 | |
| 123 | 142 | Handle nested transactions with savepoints: |
| @@ -133,7 +152,7 @@ await transaction(async (trx) => { | ||
| 133 | 152 | } |
| 134 | 153 | }) |
| 135 | 154 | |
| 136 | await trx.update('inventory').set(/_ ... _/).execute() | |
| 155 | await trx.updateTable('inventory').set(/* ... */).execute() | |
| 137 | 156 | }) |
| 138 | 157 | ``` |
| 139 | 158 | |
| @@ -149,7 +168,7 @@ try { | ||
| 149 | 168 | throw new Error('Business logic error') |
| 150 | 169 | } |
| 151 | 170 | |
| 152 | await trx.update('accounts').set({ balance: 500 }).execute() | |
| 171 | await trx.updateTable('accounts').set({ balance: 500 }).execute() | |
| 153 | 172 | }) |
| 154 | 173 | } catch (error) { |
| 155 | 174 | console.error('Transaction failed:', error.message) |
| @@ -166,7 +185,7 @@ await transaction( | ||
| 166 | 185 | async (trx) => { |
| 167 | 186 | // This might fail due to lock contention |
| 168 | 187 | await trx |
| 169 | .update('accounts') | |
| 188 | .updateTable('accounts') | |
| 170 | 189 | .set({ balance: db.raw('balance - ?', [100]) }) |
| 171 | 190 | .where({ id: 1 }) |
| 172 | 191 | .execute() |
| @@ -205,14 +224,14 @@ async function transferMoney( | ||
| 205 | 224 | } |
| 206 | 225 | |
| 207 | 226 | await trx |
| 208 | .update('accounts') | |
| 227 | .updateTable('accounts') | |
| 209 | 228 | .set({ balance: fromAccount.balance - amount }) |
| 210 | 229 | .where({ id: fromAccountId }) |
| 211 | 230 | .execute() |
| 212 | 231 | |
| 213 | 232 | // Add to destination account |
| 214 | 233 | await trx |
| 215 | .update('accounts') | |
| 234 | .updateTable('accounts') | |
| 216 | 235 | .set({ balance: db.raw('balance + ?', [amount]) }) |
| 217 | 236 | .where({ id: toAccountId }) |
| 218 | 237 | .execute() |
| @@ -277,17 +296,17 @@ async function placeOrder(userId: number, items: OrderItem[]) { | ||
| 277 | 296 | |
| 278 | 297 | // Reduce inventory |
| 279 | 298 | await trx |
| 280 | .update('products') | |
| 299 | .updateTable('products') | |
| 281 | 300 | .set({ stock: product.stock - item.quantity }) |
| 282 | 301 | .where({ id: item.productId }) |
| 283 | 302 | .execute() |
| 284 | 303 | |
| 285 | total += product.price _ item.quantity | |
| 304 | total += product.price * item.quantity | |
| 286 | 305 | } |
| 287 | 306 | |
| 288 | 307 | // Update order total |
| 289 | 308 | await trx |
| 290 | .update('orders') | |
| 309 | .updateTable('orders') | |
| 291 | 310 | .set({ total, status: 'confirmed' }) |
| 292 | 311 | .where({ id: order.id }) |
| 293 | 312 | .execute() |
| @@ -304,28 +323,32 @@ async function placeOrder(userId: number, items: OrderItem[]) { | ||
| 304 | 323 | |
| 305 | 324 | ### Batch Processing with Checkpoints |
| 306 | 325 | |
| 326 | Wrap each chunk in its own savepoint. A chunk that throws is undone on its own, | |
| 327 | and the surrounding transaction — and every chunk before it — survives. | |
| 328 | ||
| 307 | 329 | ```typescript |
| 308 | 330 | async 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) | |
| 315 | 336 | |
| 316 | 337 | 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) | |
| 326 | 347 | } |
| 327 | 348 | } |
| 328 | 349 | }) |
| 350 | ||
| 351 | return failed | |
| 329 | 352 | } |
| 330 | 353 | ``` |
| 331 | 354 | |
| @@ -8,7 +8,7 @@ Learn how to update existing data in your database using bun-query-builder. | ||
| 8 | 8 | |
| 9 | 9 | ```typescript |
| 10 | 10 | await db |
| 11 | .update('users') | |
| 11 | .updateTable('users') | |
| 12 | 12 | .set({ active: false }) |
| 13 | 13 | .where({ id: 1 }) |
| 14 | 14 | .execute() |
| @@ -18,7 +18,7 @@ await db | ||
| 18 | 18 | |
| 19 | 19 | ```typescript |
| 20 | 20 | await db |
| 21 | .update('users') | |
| 21 | .updateTable('users') | |
| 22 | 22 | .set({ |
| 23 | 23 | name: 'John Smith', |
| 24 | 24 | email: 'john.smith@example.com', |
| @@ -34,7 +34,7 @@ Return the updated rows: | ||
| 34 | 34 | |
| 35 | 35 | ```typescript |
| 36 | 36 | const updated = await db |
| 37 | .update('users') | |
| 37 | .updateTable('users') | |
| 38 | 38 | .set({ status: 'verified' }) |
| 39 | 39 | .where({ email_verified: true }) |
| 40 | 40 | .returning(['id', 'name', 'status']) |
| @@ -49,7 +49,7 @@ console.log(`Updated ${updated.length} users`) | ||
| 49 | 49 | |
| 50 | 50 | ```typescript |
| 51 | 51 | await db |
| 52 | .update('users') | |
| 52 | .updateTable('users') | |
| 53 | 53 | .set({ subscription: 'expired' }) |
| 54 | 54 | .where('subscription_ends_at', '<', new Date()) |
| 55 | 55 | .andWhere('subscription', '!=', 'lifetime') |
| @@ -60,7 +60,7 @@ await db | ||
| 60 | 60 | |
| 61 | 61 | ```typescript |
| 62 | 62 | await db |
| 63 | .update('posts') | |
| 63 | .updateTable('posts') | |
| 64 | 64 | .set({ status: 'archived' }) |
| 65 | 65 | .whereIn('id', [1, 2, 3, 4, 5]) |
| 66 | 66 | .execute() |
| @@ -73,14 +73,14 @@ await db | ||
| 73 | 73 | ```typescript |
| 74 | 74 | // Increment a column |
| 75 | 75 | await db |
| 76 | .update('posts') | |
| 76 | .updateTable('posts') | |
| 77 | 77 | .increment('views', 1) |
| 78 | 78 | .where({ id: 1 }) |
| 79 | 79 | .execute() |
| 80 | 80 | |
| 81 | 81 | // Increment by custom amount |
| 82 | 82 | await db |
| 83 | .update('users') | |
| 83 | .updateTable('users') | |
| 84 | 84 | .increment('points', 100) |
| 85 | 85 | .where({ id: userId }) |
| 86 | 86 | .execute() |
| @@ -91,14 +91,14 @@ await db | ||
| 91 | 91 | ```typescript |
| 92 | 92 | // Decrement a column |
| 93 | 93 | await db |
| 94 | .update('products') | |
| 94 | .updateTable('products') | |
| 95 | 95 | .decrement('stock', 1) |
| 96 | 96 | .where({ id: productId }) |
| 97 | 97 | .execute() |
| 98 | 98 | |
| 99 | 99 | // Ensure non-negative |
| 100 | 100 | await db |
| 101 | .update('products') | |
| 101 | .updateTable('products') | |
| 102 | 102 | .decrement('stock', quantity) |
| 103 | 103 | .where({ id: productId }) |
| 104 | 104 | .where('stock', '>=', quantity) |
| @@ -125,7 +125,7 @@ await db.updateMany( | ||
| 125 | 125 | // Update multiple records with different values |
| 126 | 126 | for (const update of updates) { |
| 127 | 127 | await db |
| 128 | .update('products') | |
| 128 | .updateTable('products') | |
| 129 | 129 | .set({ price: update.newPrice }) |
| 130 | 130 | .where({ id: update.productId }) |
| 131 | 131 | .execute() |
| @@ -135,7 +135,7 @@ for (const update of updates) { | ||
| 135 | 135 | await db.transaction(async (trx) => { |
| 136 | 136 | for (const update of updates) { |
| 137 | 137 | await trx |
| 138 | .update('products') | |
| 138 | .updateTable('products') | |
| 139 | 139 | .set({ price: update.newPrice }) |
| 140 | 140 | .where({ id: update.productId }) |
| 141 | 141 | .execute() |
| @@ -149,7 +149,7 @@ await db.transaction(async (trx) => { | ||
| 149 | 149 | |
| 150 | 150 | ```typescript |
| 151 | 151 | await db |
| 152 | .update('users') | |
| 152 | .updateTable('users') | |
| 153 | 153 | .set({ |
| 154 | 154 | login_count: sql`login_count + 1`, |
| 155 | 155 | last_login: sql`NOW()` |
| @@ -163,7 +163,7 @@ await db | ||
| 163 | 163 | ```typescript |
| 164 | 164 | // Update JSON field |
| 165 | 165 | await db |
| 166 | .update('users') | |
| 166 | .updateTable('users') | |
| 167 | 167 | .set({ |
| 168 | 168 | preferences: sql`JSON_SET(preferences, '$.theme', 'dark')` |
| 169 | 169 | }) |
| @@ -212,7 +212,7 @@ const db = createQueryBuilder({ | ||
| 212 | 212 | |
| 213 | 213 | ```typescript |
| 214 | 214 | await db |
| 215 | .update('users') | |
| 215 | .updateTable('users') | |
| 216 | 216 | .set({ deleted_at: new Date() }) |
| 217 | 217 | .where({ id: 1 }) |
| 218 | 218 | .execute() |
| @@ -222,7 +222,7 @@ await db | ||
| 222 | 222 | |
| 223 | 223 | ```typescript |
| 224 | 224 | await db |
| 225 | .update('users') | |
| 225 | .updateTable('users') | |
| 226 | 226 | .set({ deleted_at: null }) |
| 227 | 227 | .where({ id: 1 }) |
| 228 | 228 | .execute() |
| @@ -245,7 +245,7 @@ async function updateProfile(userId: number, updates: ProfileUpdate) { | ||
| 245 | 245 | } |
| 246 | 246 | |
| 247 | 247 | const user = await db |
| 248 | .update('users') | |
| 248 | .updateTable('users') | |
| 249 | 249 | .set(sanitized) |
| 250 | 250 | .where({ id: userId }) |
| 251 | 251 | .returning(['id', 'name', 'bio', 'avatar_url', 'website']) |
| @@ -263,7 +263,7 @@ async function archiveOldPosts(daysOld: number) { | ||
| 263 | 263 | cutoffDate.setDate(cutoffDate.getDate() - daysOld) |
| 264 | 264 | |
| 265 | 265 | const result = await db |
| 266 | .update('posts') | |
| 266 | .updateTable('posts') | |
| 267 | 267 | .set({ |
| 268 | 268 | status: 'archived', |
| 269 | 269 | archived_at: new Date() |
| @@ -296,7 +296,7 @@ async function processOrder(orderId: number, items: OrderItem[]) { | ||
| 296 | 296 | |
| 297 | 297 | // Decrement stock |
| 298 | 298 | await trx |
| 299 | .update('products') | |
| 299 | .updateTable('products') | |
| 300 | 300 | .decrement('stock', item.quantity) |
| 301 | 301 | .where({ id: item.product_id }) |
| 302 | 302 | .execute() |
| @@ -304,7 +304,7 @@ async function processOrder(orderId: number, items: OrderItem[]) { | ||
| 304 | 304 | |
| 305 | 305 | // Update order status |
| 306 | 306 | await trx |
| 307 | .update('orders') | |
| 307 | .updateTable('orders') | |
| 308 | 308 | .set({ status: 'processing' }) |
| 309 | 309 | .where({ id: orderId }) |
| 310 | 310 | .execute() |
| @@ -322,7 +322,7 @@ async function updateWithVersion( | ||
| 322 | 322 | currentVersion: number |
| 323 | 323 | ) { |
| 324 | 324 | const result = await db |
| 325 | .update(table) | |
| 325 | .updateTable(table) | |
| 326 | 326 | .set({ |
| 327 | 327 | ...data, |
| 328 | 328 | version: currentVersion + 1 |
| @@ -348,10 +348,10 @@ async function updateWithVersion( | ||
| 348 | 348 | |
| 349 | 349 | ```typescript |
| 350 | 350 | // Good: Targeted update with index |
| 351 | await db.update('users').set({ active: false }).where({ id: 1 }).execute() | |
| 351 | await db.updateTable('users').set({ active: false }).where({ id: 1 }).execute() | |
| 352 | 352 | |
| 353 | 353 | // Avoid: Table scan |
| 354 | await db.update('users').set({ active: false }).where('name', 'LIKE', '%test%').execute() | |
| 354 | await db.updateTable('users').set({ active: false }).where('name', 'LIKE', '%test%').execute() | |
| 355 | 355 | ``` |
| 356 | 356 | |
| 357 | 357 | ## Next Steps |
| @@ -2,6 +2,70 @@ | ||
| 2 | 2 | title: Insert Queries |
| 3 | 3 | description: Insert records into your database with type-safe queries. |
| 4 | 4 | --- |
| 5 | ||
| 6 | # Insert Queries | |
| 7 | ||
| 8 | Insert single or multiple records with full type safety. | |
| 9 | ||
| 10 | ## Basic Insert | |
| 11 | ||
| 12 | ```typescript | |
| 13 | import { createQueryBuilder } from 'bun-query-builder' | |
| 14 | ||
| 15 | const db = createQueryBuilder<typeof schema>({ schema, meta }) | |
| 16 | ||
| 17 | // Insert a single record | |
| 18 | await 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) | |
| 25 | const newUser = await db.create('users', { | |
| 26 | name: 'Jane Doe', | |
| 27 | email: 'jane@example.com', | |
| 28 | }) | |
| 29 | ||
| 30 | console.log(newUser.id) // Auto-generated ID | |
| 31 | ``` | |
| 32 | ||
| 33 | ## Insert Many Records | |
| 34 | ||
| 35 | Insert multiple records efficiently: | |
| 36 | ||
| 37 | ```typescript | |
| 38 | // Insert many records at once | |
| 39 | await 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 | ||
| 48 | Timestamps are automatically added if configured: | |
| 49 | ||
| 50 | ```typescript | |
| 51 | // If your model has timestamps enabled | |
| 52 | await 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 | ||
| 61 | Insert only if the record doesn't exist (based on unique constraints): | |
| 62 | ||
| 63 | ```typescript | |
| 64 | // Insert or ignore on conflict | |
| 65 | await db.insertOrIgnore('users', { | |
| 66 | email: 'existing@example.com', | |
| 67 | name: 'New Name', | |
| 68 | }) | |
| 5 | 69 | ``` |
| 6 | 70 | |
| 7 | 71 | ## Upsert (Insert or Update) |
| @@ -66,7 +130,7 @@ const db = createQueryBuilder<typeof schema>({ | ||
| 66 | 130 | }) |
| 67 | 131 | |
| 68 | 132 | // Hooks are called automatically |
| 69 | await db.insert('users', { | |
| 133 | await db.create('users', { | |
| 70 | 134 | name: 'John Doe', |
| 71 | 135 | email: 'john@example.com', |
| 72 | 136 | }) |
| @@ -98,7 +162,7 @@ Insert related records: | ||
| 98 | 162 | ```typescript |
| 99 | 163 | |
| 100 | 164 | // First insert the user |
| 101 | const user = await db.insert('users', { | |
| 165 | const user = await db.create('users', { | |
| 102 | 166 | name: 'John Doe', |
| 103 | 167 | email: 'john@example.com', |
| 104 | 168 | }) |
| @@ -118,7 +182,7 @@ Wrap inserts in a transaction for atomicity: | ||
| 118 | 182 | ```typescript |
| 119 | 183 | |
| 120 | 184 | await db.transaction(async (trx) => { |
| 121 | const user = await trx.insert('users', { | |
| 185 | const user = await trx.create('users', { | |
| 122 | 186 | name: 'John Doe', |
| 123 | 187 | email: 'john@example.com', |
| 124 | 188 | }) |
| @@ -178,7 +242,7 @@ const db = createQueryBuilder<typeof schema>({ | ||
| 178 | 242 | // Insert operations |
| 179 | 243 | async function createUsers() { |
| 180 | 244 | // Single insert |
| 181 | const admin = await db.insert('users', { | |
| 245 | const admin = await db.create('users', { | |
| 182 | 246 | name: 'Admin User', |
| 183 | 247 | email: 'admin@example.com', |
| 184 | 248 | active: true, |
| @@ -2,6 +2,70 @@ | ||
| 2 | 2 | title: Joins |
| 3 | 3 | description: Build type-safe JOIN queries with the query builder. |
| 4 | 4 | --- |
| 5 | ||
| 6 | # Joins | |
| 7 | ||
| 8 | Build type-safe JOIN queries to combine data from multiple tables. | |
| 9 | ||
| 10 | ## Inner Join | |
| 11 | ||
| 12 | Returns only records that have matching values in both tables: | |
| 13 | ||
| 14 | ```typescript | |
| 15 | import { createQueryBuilder } from 'bun-query-builder' | |
| 16 | ||
| 17 | const db = createQueryBuilder<typeof schema>({ schema, meta }) | |
| 18 | ||
| 19 | // Basic inner join | |
| 20 | const results = await db | |
| 21 | .selectFrom('posts') | |
| 22 | .join('users', 'posts.user_id', 'users.id') | |
| 23 | .get() | |
| 24 | ||
| 25 | // With column selection | |
| 26 | const 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 | ||
| 35 | Returns 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) | |
| 39 | const 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 | ||
| 50 | Returns all records from the right table and matching records from the left table: | |
| 51 | ||
| 52 | ```typescript | |
| 53 | // Right join | |
| 54 | const 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 | ||
| 63 | Returns the Cartesian product of both tables: | |
| 64 | ||
| 65 | ```typescript | |
| 66 | // Cross join - combine every row from both tables | |
| 67 | const combinations = await db | |
| 68 | .selectFrom('colors') | |
| 5 | 69 | .crossJoin('sizes') |
| 6 | 70 | .get() |
| 7 | 71 | |
| @@ -2,6 +2,69 @@ | ||
| 2 | 2 | title: Raw Queries |
| 3 | 3 | description: Execute raw SQL queries when you need full control. |
| 4 | 4 | --- |
| 5 | ||
| 6 | # Raw Queries | |
| 7 | ||
| 8 | Execute raw SQL queries when you need full control over the query structure. | |
| 9 | ||
| 10 | ## Raw Select | |
| 11 | ||
| 12 | Execute a raw SELECT query: | |
| 13 | ||
| 14 | ```typescript | |
| 15 | import { createQueryBuilder } from 'bun-query-builder' | |
| 16 | ||
| 17 | const db = createQueryBuilder<typeof schema>({ schema, meta }) | |
| 18 | ||
| 19 | // Raw query with parameters | |
| 20 | const users = await db.raw( | |
| 21 | 'SELECT * FROM users WHERE active = ? AND age > ?', | |
| 22 | [true, 18] | |
| 23 | ) | |
| 24 | ||
| 25 | // Using named parameters | |
| 26 | const 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 | ||
| 34 | Use raw expressions within the query builder: | |
| 35 | ||
| 36 | ```typescript | |
| 37 | // Raw in select | |
| 38 | const results = await db | |
| 39 | .selectFrom('users') | |
| 40 | .selectRaw('COUNT(*) AS total, AVG(age) AS avg_age') | |
| 41 | .get() | |
| 42 | ||
| 43 | // Raw in where | |
| 44 | const 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 | |
| 50 | const sorted = await db | |
| 51 | .selectFrom('products') | |
| 52 | .orderByRaw('price * quantity DESC') | |
| 53 | .get() | |
| 54 | ||
| 55 | // Raw in group by | |
| 56 | const 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 | |
| 63 | const filtered = await db | |
| 64 | .selectFrom('orders') | |
| 65 | .select(['user_id', 'SUM(amount) AS total']) | |
| 66 | .groupBy('user_id') | |
| 67 | .havingRaw('SUM(amount) > 1000') | |
| 5 | 68 | .get() |
| 6 | 69 | |
| 7 | 70 | ``` |
| @@ -16,7 +79,7 @@ For queries that cannot use parameterized values: | ||
| 16 | 79 | const result = await db.unsafe(` |
| 17 | 80 | SELECT * FROM users |
| 18 | 81 | WHERE email LIKE '%@example.com' |
| 19 | ORDER BY created*at DESC | |
| 82 | ORDER BY created_at DESC | |
| 20 | 83 | LIMIT 10 |
| 21 | 84 | `) |
| 22 | 85 | |
| @@ -33,13 +96,13 @@ Execute non-query SQL statements: | ||
| 33 | 96 | ```typescript |
| 34 | 97 | |
| 35 | 98 | // Create an index |
| 36 | await db.execute('CREATE INDEX idx*users*email ON users(email)') | |
| 99 | await db.unsafe('CREATE INDEX idx_users_email ON users(email)') | |
| 37 | 100 | |
| 38 | 101 | // Update statistics |
| 39 | await db.execute('ANALYZE users') | |
| 102 | await db.unsafe('ANALYZE users') | |
| 40 | 103 | |
| 41 | 104 | // Truncate table |
| 42 | await db.execute('TRUNCATE TABLE logs') | |
| 105 | await db.unsafe('TRUNCATE TABLE logs') | |
| 43 | 106 | |
| 44 | 107 | ``` |
| 45 | 108 | |
| @@ -110,14 +173,14 @@ Get typed results from raw queries: | ||
| 110 | 173 | interface UserStats { |
| 111 | 174 | country: string |
| 112 | 175 | count: number |
| 113 | avg*age: number | |
| 176 | avg_age: number | |
| 114 | 177 | } |
| 115 | 178 | |
| 116 | 179 | const stats = await db.raw<UserStats[]>(` |
| 117 | 180 | SELECT |
| 118 | 181 | country, |
| 119 | 182 | COUNT(*) AS count, |
| 120 | AVG(age) AS avg*age | |
| 183 | AVG(age) AS avg_age | |
| 121 | 184 | FROM users |
| 122 | 185 | GROUP BY country |
| 123 | 186 | ORDER BY count DESC |
| @@ -125,7 +188,7 @@ const stats = await db.raw<UserStats[]>(` | ||
| 125 | 188 | |
| 126 | 189 | // stats is typed as UserStats[] |
| 127 | 190 | stats.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}`) | |
| 129 | 192 | }) |
| 130 | 193 | |
| 131 | 194 | ``` |
| @@ -137,7 +200,7 @@ Use prepared statements for repeated queries: | ||
| 137 | 200 | ```typescript |
| 138 | 201 | |
| 139 | 202 | // Prepare a statement |
| 140 | const stmt = db.prepare('SELECT * FROM users WHERE id = ?') | |
| 203 | const stmt = db.unsafe('SELECT * FROM users WHERE id = ?') | |
| 141 | 204 | |
| 142 | 205 | // Execute multiple times efficiently |
| 143 | 206 | const user1 = await stmt.get([1]) |
| @@ -158,16 +221,16 @@ Execute raw queries within transactions: | ||
| 158 | 221 | await db.transaction(async (trx) => { |
| 159 | 222 | // Raw insert |
| 160 | 223 | await trx.raw( |
| 161 | 'INSERT INTO audit*log (action, user*id) VALUES (?, ?)', | |
| 224 | 'INSERT INTO audit_log (action, user_id) VALUES (?, ?)', | |
| 162 | 225 | ['login', userId] |
| 163 | 226 | ) |
| 164 | 227 | |
| 165 | 228 | // 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 }) | |
| 167 | 230 | |
| 168 | 231 | // Raw update |
| 169 | 232 | 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 = ?', | |
| 171 | 234 | [userId] |
| 172 | 235 | ) |
| 173 | 236 | }) |
| @@ -180,8 +243,8 @@ Analyze query execution plans: | ||
| 180 | 243 | |
| 181 | 244 | ```typescript |
| 182 | 245 | |
| 183 | // Get query execution plan | |
| 184 | const explain = await db.explain('SELECT * FROM users WHERE active = true') | |
| 246 | // Get query execution plan — `explain()` terminates a builder chain | |
| 247 | const explain = await db.selectFrom('users').where({ active: true }).explain() | |
| 185 | 248 | console.log(explain) |
| 186 | 249 | |
| 187 | 250 | // Using CLI |
| @@ -206,7 +269,7 @@ const models = { | ||
| 206 | 269 | email: { validation: { rule: {} } }, |
| 207 | 270 | age: { validation: { rule: {} } }, |
| 208 | 271 | country: { validation: { rule: {} } }, |
| 209 | created*at: { validation: { rule: {} } }, | |
| 272 | created_at: { validation: { rule: {} } }, | |
| 210 | 273 | }, |
| 211 | 274 | }, |
| 212 | 275 | } |
| @@ -220,26 +283,26 @@ async function getComplexAnalytics() { | ||
| 220 | 283 | // Complex aggregation not easily expressible with query builder |
| 221 | 284 | interface MonthlyStats { |
| 222 | 285 | 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 | |
| 226 | 289 | } |
| 227 | 290 | |
| 228 | 291 | const stats = await db.raw<MonthlyStats[]>(` |
| 229 | WITH monthly*users AS ( | |
| 292 | WITH monthly_users AS ( | |
| 230 | 293 | SELECT |
| 231 | strftime('%Y-%m', created*at) AS month, | |
| 294 | strftime('%Y-%m', created_at) AS month, | |
| 232 | 295 | id, |
| 233 | COUNT(*) OVER (PARTITION BY id) AS visit*count | |
| 296 | COUNT(*) OVER (PARTITION BY id) AS visit_count | |
| 234 | 297 | FROM users |
| 235 | WHERE created*at >= date('now', '-12 months') | |
| 298 | WHERE created_at >= date('now', '-12 months') | |
| 236 | 299 | ) |
| 237 | 300 | SELECT |
| 238 | 301 | 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 | |
| 243 | 306 | GROUP BY month |
| 244 | 307 | ORDER BY month |
| 245 | 308 | `) |
| @@ -250,7 +313,7 @@ async function getComplexAnalytics() { | ||
| 250 | 313 | .selectRaw(` |
| 251 | 314 | COUNT(*) AS total, |
| 252 | 315 | COUNT(CASE WHEN active = 1 THEN 1 END) AS active, |
| 253 | AVG(age) AS avg*age | |
| 316 | AVG(age) AS avg_age | |
| 254 | 317 | `) |
| 255 | 318 | .first() |
| 256 | 319 | |
| @@ -259,8 +322,8 @@ async function getComplexAnalytics() { | ||
| 259 | 322 | ` |
| 260 | 323 | SELECT |
| 261 | 324 | country, |
| 262 | COUNT(*) AS user*count, | |
| 263 | AVG(age) AS avg*age | |
| 325 | COUNT(*) AS user_count, | |
| 326 | AVG(age) AS avg_age | |
| 264 | 327 | FROM users |
| 265 | 328 | WHERE active = ? |
| 266 | 329 | GROUP BY country |
| @@ -2,6 +2,68 @@ | ||
| 2 | 2 | title: Select Queries |
| 3 | 3 | description: Build type-safe SELECT queries with the query builder. |
| 4 | 4 | --- |
| 5 | ||
| 6 | # Select Queries | |
| 7 | ||
| 8 | Build type-safe SELECT queries with full column and table inference. | |
| 9 | ||
| 10 | ## Basic Select | |
| 11 | ||
| 12 | ```typescript | |
| 13 | import { createQueryBuilder, buildDatabaseSchema, buildSchemaMeta } from 'bun-query-builder' | |
| 14 | ||
| 15 | const db = createQueryBuilder<typeof schema>({ schema, meta }) | |
| 16 | ||
| 17 | // Select all columns from a table | |
| 18 | const users = await db.selectFrom('users').get() | |
| 19 | ||
| 20 | // Select specific columns | |
| 21 | const names = await db | |
| 22 | .selectFrom('users') | |
| 23 | .select(['name', 'email']) | |
| 24 | .get() | |
| 25 | ||
| 26 | // Select with alias | |
| 27 | const 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 | |
| 37 | const activeUsers = await db | |
| 38 | .selectFrom('users') | |
| 39 | .where({ active: true }) | |
| 40 | .get() | |
| 41 | ||
| 42 | // Where with operator | |
| 43 | const adults = await db | |
| 44 | .selectFrom('users') | |
| 45 | .where('age', '>=', 18) | |
| 46 | .get() | |
| 47 | ||
| 48 | // Multiple conditions (AND) | |
| 49 | const activeAdults = await db | |
| 50 | .selectFrom('users') | |
| 51 | .where({ active: true }) | |
| 52 | .andWhere('age', '>=', 18) | |
| 53 | .get() | |
| 54 | ||
| 55 | // OR conditions | |
| 56 | const 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) | |
| 5 | 67 | const users = await db |
| 6 | 68 | .selectFrom('users') |
| 7 | 69 | .orderBy('name') |
| @@ -2,9 +2,75 @@ | ||
| 2 | 2 | title: Update Queries |
| 3 | 3 | description: Update records in your database with type-safe queries. |
| 4 | 4 | --- |
| 5 | ||
| 6 | # Update Queries | |
| 7 | ||
| 8 | Update records with type-safe queries and automatic timestamp handling. | |
| 9 | ||
| 10 | ## Basic Update | |
| 11 | ||
| 12 | ```typescript | |
| 13 | import { createQueryBuilder } from 'bun-query-builder' | |
| 14 | ||
| 15 | const db = createQueryBuilder<typeof schema>({ schema, meta }) | |
| 16 | ||
| 17 | // Update a single record by ID | |
| 18 | await db | |
| 19 | .updateTable('users') | |
| 20 | .set({ | |
| 21 | name: 'Updated Name', | |
| 22 | }) | |
| 23 | .where({ id: 1 }) | |
| 24 | .execute() | |
| 25 | ||
| 26 | // Update with where clause | |
| 27 | await 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 | |
| 38 | await db | |
| 39 | .updateTable('users') | |
| 40 | .set({ status: 'inactive' }) | |
| 41 | .where('last_login', '<', '2024-01-01') | |
| 42 | .execute() | |
| 43 | ||
| 44 | // Update with multiple conditions | |
| 45 | await 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 | |
| 57 | await 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 | |
| 65 | await db | |
| 66 | .updateTable('posts') | |
| 67 | .increment('views', 1) | |
| 68 | .where({ id: 123 }) | |
| 69 | .execute() | |
| 70 | ||
| 5 | 71 | // Decrement a column value |
| 6 | 72 | await db |
| 7 | .updateFrom('products') | |
| 73 | .updateTable('products') | |
| 8 | 74 | .decrement('stock', 5) |
| 9 | 75 | .where({ id: 456 }) |
| 10 | 76 | .execute() |
| @@ -18,9 +84,13 @@ If your model has timestamps enabled, `updated_at` is automatically updated: | ||
| 18 | 84 | ```typescript |
| 19 | 85 | |
| 20 | 86 | // updated_at is automatically set |
| 21 | await db.update('users', 1, { | |
| 22 | name: 'New Name', | |
| 23 | }) | |
| 87 | await db | |
| 88 | .updateTable('users') | |
| 89 | .set({ | |
| 90 | name: 'New Name', | |
| 91 | }) | |
| 92 | .where({ id: 1 }) | |
| 93 | .execute() | |
| 24 | 94 | // Equivalent to: SET name = 'New Name', updated_at = NOW() |
| 25 | 95 | |
| 26 | 96 | ``` |
| @@ -32,7 +102,7 @@ Get the updated record back: | ||
| 32 | 102 | ```typescript |
| 33 | 103 | |
| 34 | 104 | const updated = await db |
| 35 | .updateFrom('users') | |
| 105 | .updateTable('users') | |
| 36 | 106 | .set({ name: 'Updated Name' }) |
| 37 | 107 | .where({ id: 1 }) |
| 38 | 108 | .returning(['id', 'name', 'updated_at']) |
| @@ -51,7 +121,7 @@ Update only if conditions are met: | ||
| 51 | 121 | |
| 52 | 122 | // Update only active users |
| 53 | 123 | const result = await db |
| 54 | .updateFrom('users') | |
| 124 | .updateTable('users') | |
| 55 | 125 | .set({ newsletter_sent: true }) |
| 56 | 126 | .where({ active: true }) |
| 57 | 127 | .where({ newsletter_sent: false }) |
| @@ -90,16 +160,20 @@ const db = createQueryBuilder<typeof schema>({ | ||
| 90 | 160 | ```typescript |
| 91 | 161 | |
| 92 | 162 | // Update a JSON column |
| 93 | await db.update('users', 1, { | |
| 94 | preferences: { | |
| 95 | theme: 'dark', | |
| 96 | notifications: true, | |
| 97 | }, | |
| 98 | }) | |
| 163 | await db | |
| 164 | .updateTable('users') | |
| 165 | .set({ | |
| 166 | preferences: { | |
| 167 | theme: 'dark', | |
| 168 | notifications: true, | |
| 169 | }, | |
| 170 | }) | |
| 171 | .where({ id: 1 }) | |
| 172 | .execute() | |
| 99 | 173 | |
| 100 | 174 | // Update nested JSON (if supported by your database) |
| 101 | 175 | await db |
| 102 | .updateFrom('users') | |
| 176 | .updateTable('users') | |
| 103 | 177 | .set({ |
| 104 | 178 | 'preferences.theme': 'light', |
| 105 | 179 | }) |
| @@ -116,7 +190,7 @@ Efficiently update multiple records: | ||
| 116 | 190 | |
| 117 | 191 | // Update many records with the same values |
| 118 | 192 | await db |
| 119 | .updateFrom('posts') | |
| 193 | .updateTable('posts') | |
| 120 | 194 | .set({ published: true, published_at: new Date() }) |
| 121 | 195 | .where('id', 'IN', [1, 2, 3, 4, 5]) |
| 122 | 196 | .execute() |
| @@ -129,13 +203,17 @@ await db | ||
| 129 | 203 | |
| 130 | 204 | await db.transaction(async (trx) => { |
| 131 | 205 | // 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() | |
| 135 | 213 | |
| 136 | 214 | // Update related subscription |
| 137 | 215 | await trx |
| 138 | .updateFrom('subscriptions') | |
| 216 | .updateTable('subscriptions') | |
| 139 | 217 | .set({ active: true, upgraded_at: new Date() }) |
| 140 | 218 | .where({ user_id: userId }) |
| 141 | 219 | .execute() |
| @@ -203,20 +281,24 @@ const db = createQueryBuilder<typeof schema>({ | ||
| 203 | 281 | // Various update operations |
| 204 | 282 | async function updateUsers() { |
| 205 | 283 | // 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() | |
| 209 | 291 | |
| 210 | 292 | // Update with conditions |
| 211 | 293 | await db |
| 212 | .updateFrom('users') | |
| 294 | .updateTable('users') | |
| 213 | 295 | .set({ status: 'inactive' }) |
| 214 | 296 | .where('last_login', '<', '2024-01-01') |
| 215 | 297 | .execute() |
| 216 | 298 | |
| 217 | 299 | // Increment login count |
| 218 | 300 | await db |
| 219 | .updateFrom('users') | |
| 301 | .updateTable('users') | |
| 220 | 302 | .increment('login_count', 1) |
| 221 | 303 | .where({ id: 1 }) |
| 222 | 304 | .execute() |
| @@ -136,7 +136,7 @@ await db | ||
| 136 | 136 | // Conditional delete (Chris's data cleanup) |
| 137 | 137 | await db |
| 138 | 138 | .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)]) | |
| 140 | 140 | .execute() |
| 141 | 141 | ``` |
| 142 | 142 | |
| @@ -381,7 +381,7 @@ query-builder file ./scripts/analytics.sql --output results.json | ||
| 381 | 381 | |
| 382 | 382 | ```bash |
| 383 | 383 | # Performance analysis for Chris's dashboard queries |
| 384 | query-builder explain "SELECT _ FROM users JOIN teams ON users.team_id = teams.id WHERE users.active = true" | |
| 384 | query-builder explain "SELECT * FROM users JOIN teams ON users.team_id = teams.id WHERE users.active = true" | |
| 385 | 385 | |
| 386 | 386 | # Avery's e-commerce query optimization |
| 387 | 387 | query-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 | ||
| 394 | 394 | |
| 395 | 395 | ```bash |
| 396 | 396 | # Parameterized queries (use with caution) |
| 397 | query-builder unsafe "SELECT _ FROM users WHERE id = $1 AND team_id = $2" --params "[1, 3]" | |
| 397 | query-builder unsafe "SELECT * FROM users WHERE id = $1 AND team_id = $2" --params "[1, 3]" | |
| 398 | 398 | |
| 399 | 399 | # Bulk operations with transaction support |
| 400 | 400 | query-builder file ./scripts/bulk_update.sql --transaction --batch-size 1000 |