also looking at this
fix(client)!: bind db.raw and db.sql fragment values or refuse them
#1152
16 files
+455
-118
| @@ -27,7 +27,7 @@ import { | ||
| 27 | 27 | } from 'bun-query-builder' |
| 28 | 28 | |
| 29 | 29 | // Example: Chris's team setup |
| 30 | import { buildDatabaseSchema, buildSchemaMeta, config, createQueryBuilder } from 'bun-query-builder' | |
| 30 | import { buildDatabaseSchema, buildSchemaMeta, config, createQueryBuilder, raw } from 'bun-query-builder' | |
| 31 | 31 | import { userModels } from './models' |
| 32 | 32 | |
| 33 | 33 | // Configure for PostgreSQL production environment |
| @@ -87,9 +87,10 @@ setConfig({ transactionDefaults: { retries: 5 } }) | ||
| 87 | 87 | - updateTable(table) |
| 88 | 88 | - deleteFrom(table) |
| 89 | 89 | - sql: passthrough to Bun’s `sql` |
| 90 | - raw(strings, ...values) | |
| 90 | - raw(sql, bindings?): bound `{ sql, parameters }` fragment for `set()` / `where()` (not a query; awaiting it rejects) | |
| 91 | - ``raw`...` `` (tagged template): passthrough to Bun’s `sql` | |
| 91 | 92 | - simple(strings, ...values) |
| 92 | - unsafe(query, params?) | |
| 93 | - unsafe(query, params?): run SQL and return rows | |
| 93 | 94 | - file(path, params?) |
| 94 | 95 | - reserve(): Promise<QueryBuilder & { release() }> |
| 95 | 96 | - close(opts?) |
| @@ -234,7 +235,7 @@ setConfig({ transactionDefaults: { retries: 5 } }) | ||
| 234 | 235 | const userAnalytics = await db |
| 235 | 236 | .selectFrom('users') |
| 236 | 237 | .select('users', 'id', 'name', 'email', 'created_at') |
| 237 | .selectRaw(db.sql`COUNT(posts.id) as post_count`) | |
| 238 | .selectRaw(raw`COUNT(posts.id) as post_count`) | |
| 238 | 239 | .with('Profile', 'Team') |
| 239 | 240 | .leftJoin('posts', 'posts.author_id', '=', 'users.id') |
| 240 | 241 | .where({ 'users.active': true }) |
| @@ -350,13 +350,15 @@ function optimizeForSQLite() { | ||
| 350 | 350 | - **Migration Strategy**: Plan for potential dialect changes |
| 351 | 351 | |
| 352 | 352 | ```ts |
| 353 | import { raw } from 'bun-query-builder' | |
| 354 | ||
| 353 | 355 | // Feature detection pattern |
| 354 | 356 | function getRandomOrderQuery() { |
| 355 | 357 | const randomFunction = config.sql.randomFunction || 'RANDOM()' |
| 356 | 358 | |
| 357 | 359 | return db |
| 358 | 360 | .selectFrom('quotes') |
| 359 | .orderByRaw(db.sql`${randomFunction}`) | |
| 361 | .orderByRaw(raw(randomFunction)) | |
| 360 | 362 | .limit(1) |
| 361 | 363 | } |
| 362 | 364 | |
| @@ -107,7 +107,7 @@ async function processOrder(orderData: { | ||
| 107 | 107 | // Reserve inventory for each item |
| 108 | 108 | for (const item of orderData.items) { |
| 109 | 109 | await tx.updateTable('products') |
| 110 | .set({ reserved_stock: db.sql`reserved_stock + ${item.quantity}` }) | |
| 110 | .set({ reserved_stock: db.raw('reserved_stock + ?', [item.quantity]) }) | |
| 111 | 111 | .where({ id: item.productId }) |
| 112 | 112 | .execute() |
| 113 | 113 | |
| @@ -159,6 +159,7 @@ const highValueCustomers = await db | ||
| 159 | 159 | Use raw SQL for complex aggregations: |
| 160 | 160 | |
| 161 | 161 | ```typescript |
| 162 | import { raw } from 'bun-query-builder' | |
| 162 | 163 | |
| 163 | 164 | // Custom aggregation |
| 164 | 165 | const results = await db |
| @@ -173,12 +174,12 @@ const results = await db | ||
| 173 | 174 | // Group by with raw expressions |
| 174 | 175 | const weeklyStats = await db |
| 175 | 176 | .selectFrom('orders') |
| 176 | .selectRaw(` | |
| 177 | .selectRaw(raw` | |
| 177 | 178 | strftime('%Y-%W', created_at) AS week, |
| 178 | COUNT(_) AS orders, | |
| 179 | COUNT(*) AS orders, | |
| 179 | 180 | SUM(amount) AS revenue |
| 180 | 181 | `) |
| 181 | .groupByRaw("strftime('%Y-%W', created_at)") | |
| 182 | .groupByRaw(raw`strftime('%Y-%W', created_at)`) | |
| 182 | 183 | .get() |
| 183 | 184 | |
| 184 | 185 | ``` |
| @@ -237,7 +238,7 @@ const authorStats = await db | ||
| 237 | 238 | |
| 238 | 239 | ```typescript |
| 239 | 240 | |
| 240 | import { createQueryBuilder, buildDatabaseSchema, buildSchemaMeta } from 'bun-query-builder' | |
| 241 | import { createQueryBuilder, buildDatabaseSchema, buildSchemaMeta, raw } from 'bun-query-builder' | |
| 241 | 242 | |
| 242 | 243 | // Setup |
| 243 | 244 | const models = { |
| @@ -315,12 +316,12 @@ async function getAnalytics() { | ||
| 315 | 316 | // Monthly revenue trend |
| 316 | 317 | const monthlyRevenue = await db |
| 317 | 318 | .selectFrom('orders') |
| 318 | .selectRaw(` | |
| 319 | .selectRaw(raw` | |
| 319 | 320 | strftime('%Y-%m', created_at) AS month, |
| 320 | COUNT(_) AS orders, | |
| 321 | COUNT(*) AS orders, | |
| 321 | 322 | SUM(amount) AS revenue |
| 322 | 323 | `) |
| 323 | .groupByRaw("strftime('%Y-%m', created_at)") | |
| 324 | .groupByRaw(raw`strftime('%Y-%m', created_at)`) | |
| 324 | 325 | .orderBy('month') |
| 325 | 326 | .get() |
| 326 | 327 | |
| @@ -49,7 +49,7 @@ const meta = buildSchemaMeta(models) | ||
| 49 | 49 | |--------|-------------|---------| |
| 50 | 50 | | `selectFrom(table)` | Start a SELECT query | `db.selectFrom('users')` | |
| 51 | 51 | | `select(columns)` | Select specific columns | `.select(['id', 'name'])` | |
| 52 | | `selectRaw(sql)` | Select with raw SQL | `.selectRaw('COUNT(*) as count')` | | |
| 52 | | `selectRaw(fragment)` | Select with raw SQL (one fragment, no bindings) | ``.selectRaw(raw`COUNT(*) as count`)`` | | |
| 53 | 53 | | `distinct()` | Add DISTINCT modifier | `.distinct()` | |
| 54 | 54 | | `distinctOn(columns)` | DISTINCT ON (PostgreSQL) | `.distinctOn('email')` | |
| 55 | 55 | |
| @@ -48,6 +48,8 @@ const recentOrders = await db | ||
| 48 | 48 | ## Selecting Data |
| 49 | 49 | |
| 50 | 50 | ```ts |
| 51 | import { raw } from 'bun-query-builder' | |
| 52 | ||
| 51 | 53 | // Select all columns from projects |
| 52 | 54 | const allProjects = await db |
| 53 | 55 | .selectFrom('projects') |
| @@ -64,7 +66,7 @@ const userInfo = await db | ||
| 64 | 66 | const userStats = await db |
| 65 | 67 | .selectFrom('users') |
| 66 | 68 | .select('users', 'id', 'name') |
| 67 | .selectRaw(db.sql`COUNT(posts.id) as post_count`) | |
| 69 | .selectRaw(raw`COUNT(posts.id) as post_count`) | |
| 68 | 70 | .leftJoin('posts', 'posts.user_id', '=', 'users.id') |
| 69 | 71 | .groupBy('users.id', 'users.name') |
| 70 | 72 | .execute() |
| @@ -74,7 +76,7 @@ const userProfiles = await db | ||
| 74 | 76 | .selectFrom('users') |
| 75 | 77 | .innerJoin('profiles', 'profiles.user_id', '=', 'users.id') |
| 76 | 78 | .select('users', 'id', 'name') |
| 77 | .selectRaw(db.sql`profiles.bio, profiles.avatar_url`) | |
| 79 | .selectRaw(raw`profiles.bio, profiles.avatar_url`) | |
| 78 | 80 | .where({ 'users.active': true }) |
| 79 | 81 | .execute() |
| 80 | 82 | ``` |
| @@ -82,7 +84,7 @@ const userProfiles = await db | ||
| 82 | 84 | ### Aliases and Expressions |
| 83 | 85 | |
| 84 | 86 | - Use `"column as alias"` syntax in `select(table, ...)` for simple aliases |
| 85 | - Use `selectRaw(db.sql\`...\`)` for complex expressions, functions, and computed columns | |
| 87 | - Use ``selectRaw(raw`...`)`` (the `raw` helper exported by `bun-query-builder`) for complex expressions, functions, and computed columns | |
| 86 | 88 | - Combine `select()` and `selectRaw()` calls to build comprehensive column lists |
| 87 | 89 | - Always prefix column names with table names when joining to avoid ambiguity |
| 88 | 90 | |
| @@ -375,9 +377,9 @@ Use raw sparingly for complex cases not covered by helpers. | ||
| 375 | 377 | ```ts |
| 376 | 378 | await db |
| 377 | 379 | .selectFrom('users') |
| 378 | .whereRaw(db.sql`coalesce(age, 0) > 0`) | |
| 379 | .groupByRaw(db.sql`country`) | |
| 380 | .havingRaw(db.sql`count(_) > 10`) | |
| 380 | .whereRaw(raw`coalesce(age, 0) > 0`) | |
| 381 | .groupByRaw(raw`country`) | |
| 382 | .havingRaw(raw`count(*) > 10`) | |
| 381 | 383 | .execute() |
| 382 | 384 | ``` |
| 383 | 385 | |
| @@ -684,7 +686,7 @@ await db | ||
| 684 | 686 | // Update with complex conditions |
| 685 | 687 | await db |
| 686 | 688 | .updateTable('users') |
| 687 | .set({ last_login: new Date(), login_count: db.sql`login_count + 1` }) | |
| 689 | .set({ last_login: new Date(), login_count: db.raw('login_count + 1') }) | |
| 688 | 690 | .where({ email: 'chris@example.com' }) |
| 689 | 691 | .andWhere(['active', '=', true]) |
| 690 | 692 | .execute() |
| @@ -869,7 +871,7 @@ const users = await db | ||
| 869 | 871 | // Avoid: Excessive raw SQL |
| 870 | 872 | const users = await db |
| 871 | 873 | .selectFrom('users') |
| 872 | .whereRaw(db.sql`active = true ${includeAdmin ? 'OR role = "admin"' : ''}`) | |
| 874 | .whereRaw(raw(`active = true${includeAdmin ? " OR role = 'admin'" : ''}`)) | |
| 873 | 875 | .execute() |
| 874 | 876 | ``` |
| 875 | 877 | |
| @@ -917,7 +919,7 @@ const user = await db | ||
| 917 | 919 | // Dangerous: String interpolation |
| 918 | 920 | const user = await db |
| 919 | 921 | .selectFrom('users') |
| 920 | .whereRaw(db.sql`email = '${userInput.email}'`) // DON'T DO THIS | |
| 922 | .whereRaw(raw(`email = '${userInput.email}'`)) // DON'T DO THIS: the string is built before raw() sees it | |
| 921 | 923 | ``` |
| 922 | 924 | |
| 923 | 925 | ### Error Handling and Monitoring |
renamed throughout · 16 hidden lines
| @@ -1071,14 +1073,14 @@ const searchQuery = 'TypeScript' renamed throughout | ||
| 1071 | 1073 | const articleResults = db |
| 1072 | 1074 | .selectFrom('articles') |
| 1073 | 1075 | .select('articles', 'id', 'title', 'created_at') |
| 1074 | .selectRaw(db.sql`'article' as content_type`) | |
| 1076 | .selectRaw(raw`'article' as content_type`) | |
| 1075 | 1077 | .where(['title', 'like', `%${searchQuery}%`]) |
| 1076 | 1078 | .orWhere(['content', 'like', `%${searchQuery}%`]) |
| 1077 | 1079 | |
| 1078 | 1080 | const projectResults = db |
| 1079 | 1081 | .selectFrom('projects') |
| 1080 | 1082 | .select('projects', 'id', 'name as title', 'created_at') |
| 1081 | .selectRaw(db.sql`'project' as content_type`) | |
| 1083 | .selectRaw(raw`'project' as content_type`) | |
| 1082 | 1084 | .where(['name', 'like', `%${searchQuery}%`]) |
| 1083 | 1085 | .orWhere(['description', 'like', `%${searchQuery}%`]) |
| 1084 | 1086 | |
| @@ -1099,7 +1101,7 @@ const userStats = await db | ||
| 1099 | 1101 | .withCount('posts', 'total_posts') |
| 1100 | 1102 | .withCount('posts', 'published_posts', ['published', '=', true]) |
| 1101 | 1103 | .withCount('comments', 'total_comments') |
| 1102 | .selectRaw(db.sql` | |
| 1104 | .selectRaw(raw` | |
| 1103 | 1105 | CASE |
| 1104 | 1106 | WHEN COUNT(posts.id) > 10 THEN 'high' |
| 1105 | 1107 | WHEN COUNT(posts.id) > 5 THEN 'medium' |
| @@ -1114,13 +1116,13 @@ const userStats = await db | ||
| 1114 | 1116 | // Monthly user registration trends |
| 1115 | 1117 | const registrationTrends = await db |
| 1116 | 1118 | .selectFrom('users') |
| 1117 | .selectRaw(db.sql` | |
| 1119 | .selectRaw(raw` | |
| 1118 | 1120 | DATE_TRUNC('month', created_at) as month, |
| 1119 | COUNT(_) as new_users, | |
| 1121 | COUNT(*) as new_users, | |
| 1120 | 1122 | COUNT(CASE WHEN role = 'admin' THEN 1 END) as new_admins |
| 1121 | 1123 | `) |
| 1122 | 1124 | .where(['created_at', '>=', new Date('2024-01-01')]) |
| 1123 | .groupBy(db.sql`DATE_TRUNC('month', created_at)`) | |
| 1125 | .groupByRaw(raw`DATE_TRUNC('month', created_at)`) | |
| 1124 | 1126 | .orderBy('month', 'desc') |
| 1125 | 1127 | .execute() |
| 1126 | 1128 | ``` |
| @@ -1144,7 +1146,7 @@ const chrisTeamWithStats = await db | ||
| 1144 | 1146 | const projectsWithCollaborators = await db |
| 1145 | 1147 | .selectFrom('projects') |
| 1146 | 1148 | .select('projects', 'id', 'name', 'description') |
| 1147 | .selectRaw(db.sql` | |
| 1149 | .selectRaw(raw` | |
| 1148 | 1150 | STRING_AGG(users.name, ', ') as collaborators, |
| 1149 | 1151 | COUNT(DISTINCT project_users.user_id) as collaborator_count |
| 1150 | 1152 | `) |
| @@ -1226,7 +1228,7 @@ Use `whereColumn(left, op, right)`. | ||
| 1226 | 1228 | |
| 1227 | 1229 | ### How do I add arbitrary fragments |
| 1228 | 1230 | |
| 1229 | Use `selectRaw`, `whereRaw`, `groupByRaw`, or `havingRaw` and pass a Bun `sql` fragment. | |
| 1231 | Use `selectRaw`, `whereRaw`, `orderByRaw`, `groupByRaw`, or `havingRaw` and pass one fragment built with the exported `raw` helper: ``raw`count(*) as c` `` or `raw('age > 18')`. These methods take no bindings, and values interpolated into ``raw`...` `` are inlined as escaped literals. To bind values, pass `db.raw('name = ?', [value])` or `{ sql, parameters }` to `where()` or `set()`. A Bun ``db.sql`...` `` query is not a fragment: it throws on Postgres and MySQL. | |
| 1230 | 1232 | |
| 1231 | 1233 | --- |
| 1232 | 1234 | |
| @@ -462,11 +462,13 @@ the model layer's eager loading. | ||
| 462 | 462 | Example: |
| 463 | 463 | |
| 464 | 464 | ```ts |
| 465 | import { raw } from 'bun-query-builder' | |
| 466 | ||
| 465 | 467 | await db |
| 466 | 468 | .selectFrom('users') |
| 467 | 469 | .with('Project') |
| 468 | 470 | .select('users', 'id', 'email') |
| 469 | .selectRaw(db.sql`projects.name as projects_name`) | |
| 471 | .selectRaw(raw`projects.name as projects_name`) | |
| 470 | 472 | .execute() |
| 471 | 473 | ``` |
| 472 | 474 | |
renamed throughout · 10 hidden lines
| @@ -508,8 +510,8 @@ await db renamed throughout | ||
| 508 | 510 | .selectFrom('users') |
| 509 | 511 | .with('Project') |
| 510 | 512 | .select('users', 'id', 'email') |
| 511 | .selectRaw(db.sql`projects.id as projects_id`) | |
| 512 | .selectRaw(db.sql`projects.name as projects_name`) | |
| 513 | .selectRaw(raw`projects.id as projects_id`) | |
| 514 | .selectRaw(raw`projects.name as projects_name`) | |
| 513 | 515 | .execute() |
| 514 | 516 | ``` |
| 515 | 517 | |
| @@ -520,7 +522,7 @@ await db | ||
| 520 | 522 | .selectFrom('users') |
| 521 | 523 | .with('Project') |
| 522 | 524 | .groupBy('users.id') |
| 523 | .selectRaw(db.sql`COUNT(projects.id) as projects_count`) | |
| 525 | .selectRaw(raw`COUNT(projects.id) as projects_count`) | |
| 524 | 526 | .execute() |
| 525 | 527 | ``` |
| 526 | 528 | |
| @@ -568,7 +570,7 @@ const teamData = await db | ||
| 568 | 570 | .selectFrom('users') |
| 569 | 571 | .with('Project') |
| 570 | 572 | .select('users', 'id', 'name', 'role') |
| 571 | .selectRaw(db.sql`projects.id as project_id, projects.name as project_name`) | |
| 573 | .selectRaw(raw`projects.id as project_id, projects.name as project_name`) | |
| 572 | 574 | .where({ 'users.team': 'Engineering' }) |
| 573 | 575 | .execute() |
| 574 | 576 | ``` |
| @@ -671,7 +673,7 @@ await db | ||
| 671 | 673 | .with('Project') |
| 672 | 674 | .whereHas('Project', ['status', '=', 'active']) |
| 673 | 675 | .groupBy('users.id') |
| 674 | .havingRaw(db.sql`COUNT(CASE WHEN projects.status = 'inactive' THEN 1 END) = 0`) | |
| 676 | .havingRaw(raw`COUNT(CASE WHEN projects.status = 'inactive' THEN 1 END) = 0`) | |
| 675 | 677 | .execute() |
| 676 | 678 | ``` |
| 677 | 679 | |
| @@ -682,7 +684,7 @@ await db | ||
| 682 | 684 | .selectFrom('users') |
| 683 | 685 | .with('Project') |
| 684 | 686 | .groupBy('users.id') |
| 685 | .selectRaw(db.sql`COUNT(projects.id) as projects_count`) | |
| 687 | .selectRaw(raw`COUNT(projects.id) as projects_count`) | |
| 686 | 688 | .orderByDesc('projects_count' as any) |
| 687 | 689 | .limit(10) |
| 688 | 690 | .execute() |
| @@ -696,7 +698,7 @@ await db | ||
| 696 | 698 | .selectFrom('users') |
| 697 | 699 | .with('Project') |
| 698 | 700 | .groupBy('users.id') |
| 699 | .havingRaw(db.sql`COUNT(projects.id) >= ${db.sql(String(N))}`) | |
| 701 | .havingRaw(raw`COUNT(projects.id) >= ${N}`) | |
| 700 | 702 | .execute() |
| 701 | 703 | ``` |
| 702 | 704 | |
| @@ -740,7 +742,7 @@ await db | ||
| 740 | 742 | .selectFrom('users') |
| 741 | 743 | .join('projects', 'projects.ownerId' as any, '=', 'users.uid' as any) |
| 742 | 744 | .select('users', 'uid as id', 'email') |
| 743 | .selectRaw(db.sql`projects.title as projects_title`) | |
| 745 | .selectRaw(raw`projects.title as projects_title`) | |
| 744 | 746 | .execute() |
| 745 | 747 | ``` |
| 746 | 748 | |
renamed throughout · 10 hidden lines
| @@ -782,8 +784,8 @@ await db renamed throughout | ||
| 782 | 784 | .selectFrom('users') |
| 783 | 785 | .with('Project') |
| 784 | 786 | .select('users', 'id', 'email') |
| 785 | .selectRaw(db.sql`projects.id as projects_id`) | |
| 786 | .selectRaw(db.sql`projects.status as projects_status`) | |
| 787 | .selectRaw(raw`projects.id as projects_id`) | |
| 788 | .selectRaw(raw`projects.status as projects_status`) | |
| 787 | 789 | .execute() |
| 788 | 790 | ``` |
| 789 | 791 | |
renamed throughout · 10 hidden lines
| @@ -794,8 +796,8 @@ await db renamed throughout | ||
| 794 | 796 | .selectFrom('users') |
| 795 | 797 | .with('Project') |
| 796 | 798 | .groupBy('users.id') |
| 797 | .selectRaw(db.sql`COUNT(projects.id) as projects_count`) | |
| 798 | .selectRaw(db.sql`SUM(CASE WHEN projects.status = 'active' THEN 1 ELSE 0 END) as active_count`) | |
| 799 | .selectRaw(raw`COUNT(projects.id) as projects_count`) | |
| 800 | .selectRaw(raw`SUM(CASE WHEN projects.status = 'active' THEN 1 ELSE 0 END) as active_count`) | |
| 799 | 801 | .execute() |
| 800 | 802 | ``` |
| 801 | 803 | |
| @@ -251,12 +251,12 @@ async function transferFunds(fromAccount: number, toAccount: number, amount: num | ||
| 251 | 251 | } |
| 252 | 252 | |
| 253 | 253 | await tx.updateTable('accounts') |
| 254 | .set({ balance: db.sql`balance - ${amount}` }) | |
| 254 | .set({ balance: db.raw('balance - ?', [amount]) }) | |
| 255 | 255 | .where({ id: fromAccount }) |
| 256 | 256 | .execute() |
| 257 | 257 | |
| 258 | 258 | await tx.updateTable('accounts') |
| 259 | .set({ balance: db.sql`balance + ${amount}` }) | |
| 259 | .set({ balance: db.raw('balance + ?', [amount]) }) | |
| 260 | 260 | .where({ id: toAccount }) |
| 261 | 261 | .execute() |
| 262 | 262 | }, { isolation: 'serializable', retries: 3 }) |
| @@ -288,7 +288,7 @@ async function highContentionOperation(data: any) { | ||
| 288 | 288 | return await db.transaction(async (tx) => { |
| 289 | 289 | // Operations that might conflict with other transactions |
| 290 | 290 | await tx.updateTable('counters') |
| 291 | .set({ value: db.sql`value + 1` }) | |
| 291 | .set({ value: db.raw('value + 1') }) | |
| 292 | 292 | .where({ name: 'page_views' }) |
| 293 | 293 | .execute() |
| 294 | 294 | }, { |
| @@ -318,7 +318,7 @@ async function processOrder(orderData: any) { | ||
| 318 | 318 | |
| 319 | 319 | // Update inventory |
| 320 | 320 | await tx.updateTable('products') |
| 321 | .set({ stock_quantity: db.sql`stock_quantity - ${orderData.quantity}` }) | |
| 321 | .set({ stock_quantity: db.raw('stock_quantity - ?', [orderData.quantity]) }) | |
| 322 | 322 | .where({ id: orderData.product_id }) |
| 323 | 323 | .execute() |
| 324 | 324 | |
| @@ -448,11 +448,13 @@ await db.transaction(async (tx) => { | ||
| 448 | 448 | ### Serializable reads with retries |
| 449 | 449 | |
| 450 | 450 | ```ts |
| 451 | import { raw } from 'bun-query-builder' | |
| 452 | ||
| 451 | 453 | await db.transaction(async (tx) => { |
| 452 | 454 | const totals = await tx |
| 453 | 455 | .selectFrom('orders') |
| 454 | 456 | .groupBy('customer_id') |
| 455 | .selectRaw(db.sql`SUM(total) as total`) | |
| 457 | .selectRaw(raw`SUM(total) as total`) | |
| 456 | 458 | .execute() |
| 457 | 459 | // ...work with totals |
| 458 | 460 | }, { isolation: 'serializable', retries: 2 }) |
| @@ -151,8 +151,8 @@ await db.transaction(async (trx) => { | ||
| 151 | 151 | await db |
| 152 | 152 | .updateTable('users') |
| 153 | 153 | .set({ |
| 154 | login_count: sql`login_count + 1`, | |
| 155 | last_login: sql`NOW()` | |
| 154 | login_count: db.raw('login_count + 1'), | |
| 155 | last_login: db.raw('CURRENT_TIMESTAMP') | |
| 156 | 156 | }) |
| 157 | 157 | .where({ id: userId }) |
| 158 | 158 | .execute() |
| @@ -165,7 +165,7 @@ await db | ||
| 165 | 165 | await db |
| 166 | 166 | .updateTable('users') |
| 167 | 167 | .set({ |
| 168 | preferences: sql`JSON_SET(preferences, '$.theme', 'dark')` | |
| 168 | preferences: db.raw("JSON_SET(preferences, '$.theme', 'dark')") | |
| 169 | 169 | }) |
| 170 | 170 | .where({ id: userId }) |
| 171 | 171 | .execute() |
| @@ -16,47 +16,54 @@ import { createQueryBuilder } from 'bun-query-builder' | ||
| 16 | 16 | |
| 17 | 17 | const db = createQueryBuilder<typeof schema>({ schema, meta }) |
| 18 | 18 | |
| 19 | // Raw query with parameters | |
| 20 | const users = await db.raw( | |
| 19 | // Raw query with parameters (SQLite and MySQL use `?` placeholders) | |
| 20 | const users = await db.unsafe( | |
| 21 | 21 | 'SELECT * FROM users WHERE active = ? AND age > ?', |
| 22 | 22 | [true, 18] |
| 23 | 23 | ) |
| 24 | 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 } | |
| 25 | // Postgres uses numbered placeholders | |
| 26 | const posts = await db.unsafe( | |
| 27 | 'SELECT * FROM posts WHERE user_id = $1 AND published = $2', | |
| 28 | [1, true] | |
| 29 | 29 | ) |
| 30 | 30 | ``` |
| 31 | 31 | |
| 32 | `db.unsafe(sql, params)` runs the statement and resolves to its rows. `db.raw(sql, bindings)` | |
| 33 | does not run anything: it builds a fragment for `set()` and `where()` (see | |
| 34 | [Bound Fragments](#bound-fragments-in-set-and-where)), and awaiting it rejects. | |
| 35 | ||
| 32 | 36 | ## Raw Expressions in Queries |
| 33 | 37 | |
| 34 | 38 | Use raw expressions within the query builder: |
| 35 | 39 | |
| 36 | 40 | ```typescript |
| 41 | import { raw } from 'bun-query-builder' | |
| 42 | ||
| 37 | 43 | // Raw in select |
| 38 | 44 | const results = await db |
| 39 | 45 | .selectFrom('users') |
| 40 | .selectRaw('COUNT(*) AS total, AVG(age) AS avg_age') | |
| 46 | .selectRaw(raw`COUNT(*) AS total, AVG(age) AS avg_age`) | |
| 41 | 47 | .get() |
| 42 | 48 | |
| 43 | // Raw in where | |
| 49 | // Raw in where (the interpolated Date is inlined as an escaped literal) | |
| 50 | const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) | |
| 44 | 51 | const recentUsers = await db |
| 45 | 52 | .selectFrom('users') |
| 46 | .whereRaw('DATE(created_at) > DATE_SUB(NOW(), INTERVAL 30 DAY)') | |
| 53 | .whereRaw(raw`created_at > ${since}`) | |
| 47 | 54 | .get() |
| 48 | 55 | |
| 49 | 56 | // Raw in order by |
| 50 | 57 | const sorted = await db |
| 51 | 58 | .selectFrom('products') |
| 52 | .orderByRaw('price * quantity DESC') | |
| 59 | .orderByRaw(raw`price * quantity DESC`) | |
| 53 | 60 | .get() |
| 54 | 61 | |
| 55 | 62 | // Raw in group by |
| 56 | 63 | const grouped = await db |
| 57 | 64 | .selectFrom('orders') |
| 58 | 65 | .select(['SUM(amount) AS total']) |
| 59 | .groupByRaw("strftime('%Y-%m', created_at)") | |
| 66 | .groupByRaw(raw`strftime('%Y-%m', created_at)`) | |
| 60 | 67 | .get() |
| 61 | 68 | |
| 62 | 69 | // Raw in having |
| @@ -64,7 +71,7 @@ const filtered = await db | ||
| 64 | 71 | .selectFrom('orders') |
| 65 | 72 | .select(['user_id', 'SUM(amount) AS total']) |
| 66 | 73 | .groupBy('user_id') |
| 67 | .havingRaw('SUM(amount) > 1000') | |
| 74 | .havingRaw(raw`SUM(amount) > 1000`) | |
| 68 | 75 | .get() |
| 69 | 76 | |
| 70 | 77 | ``` |
| @@ -123,13 +130,48 @@ await db.selectFrom('users').orderByRaw(raw`created_at desc`).get() | ||
| 123 | 130 | await db.selectFrom('orders').whereRaw(raw`status = ${userStatus}`).get() |
| 124 | 131 | ``` |
| 125 | 132 | |
| 126 | > **Do not pass a Bun `sql\`...\`` query to the `*Raw` methods.** A Bun query | |
| 127 | > object cannot be converted back to SQL text (it stringifies to | |
| 128 | > `"[object Promise]"`), so it would corrupt the generated SQL. The builder | |
| 129 | > now throws a clear error if you do. `raw` returns a `{ raw }` fragment that | |
| 130 | > renders correctly and satisfies the `SqlFragment` type (so it still passes | |
| 131 | > the bare-string injection guard). For user input that must be | |
| 132 | > parameterised, prefer the typed `where(...)` methods over `raw`. | |
| 133 | The `*Raw` methods take exactly one fragment and no bindings. Passing extra | |
| 134 | arguments, or a fragment that carries bound values (`db.raw('x = ?', [v])`), | |
| 135 | throws instead of silently dropping the values. Bare strings still work at | |
| 136 | runtime but log a deprecation warning and are a TypeScript error. | |
| 137 | ||
| 138 | > **Do not pass a Bun ``sql`...` `` query to builder methods.** A Bun query | |
| 139 | > object cannot be converted back to SQL text, so on Postgres and MySQL the | |
| 140 | > builder throws a clear error if you pass one to a `*Raw` method, `where()`, | |
| 141 | > or `set()`. `raw` returns a `{ raw }` fragment that renders correctly and | |
| 142 | > satisfies the `SqlFragment` type. Values interpolated into ``raw`...` `` are | |
| 143 | > inlined as escaped literals, not bound; for user input that must be | |
| 144 | > parameterised, use `where(...)` or a bound fragment (below). | |
| 145 | ||
| 146 | ## Bound Fragments in `set()` and `where()` | |
| 147 | ||
| 148 | To bind values inside a SQL expression, build a fragment with | |
| 149 | `db.raw(sql, bindings)`. Each `?` binds the matching array element, on every | |
| 150 | dialect (the builder renumbers them to `$n` on Postgres). A plain | |
| 151 | `{ sql, parameters }` object works the same way. | |
| 152 | ||
| 153 | ```typescript | |
| 154 | // Expression in set() | |
| 155 | await db | |
| 156 | .updateTable('products') | |
| 157 | .set({ stock: db.raw('stock - ?', [quantity]) }) | |
| 158 | .where({ id: productId }) | |
| 159 | .execute() | |
| 160 | ||
| 161 | // Condition in where() / orWhere(), on select, update and delete builders | |
| 162 | const matches = await db | |
| 163 | .selectFrom('users') | |
| 164 | .where(db.raw('LOWER(email) = ?', [email.toLowerCase()])) | |
| 165 | .get() | |
| 166 | ||
| 167 | await db | |
| 168 | .deleteFrom('sessions') | |
| 169 | .where({ sql: 'expires_at < ?', parameters: [new Date().toISOString()] }) | |
| 170 | .execute() | |
| 171 | ``` | |
| 172 | ||
| 173 | `db.raw(sql, bindings)` is not a query: `await db.raw('SELECT ...', [..])` | |
| 174 | rejects with a `TypeError`. Use `db.unsafe(sql, params)` to run SQL. | |
| 133 | 175 | |
| 134 | 176 | ## Raw with Bun Tagged Templates |
| 135 | 177 | |
| @@ -176,7 +218,7 @@ interface UserStats { | ||
| 176 | 218 | avg_age: number |
| 177 | 219 | } |
| 178 | 220 | |
| 179 | const stats = await db.raw<UserStats[]>(` | |
| 221 | const stats = await db.unsafe<UserStats>(` | |
| 180 | 222 | SELECT |
| 181 | 223 | country, |
| 182 | 224 | COUNT(*) AS count, |
| @@ -220,7 +262,7 @@ Execute raw queries within transactions: | ||
| 220 | 262 | |
| 221 | 263 | await db.transaction(async (trx) => { |
| 222 | 264 | // Raw insert |
| 223 | await trx.raw( | |
| 265 | await trx.unsafe( | |
| 224 | 266 | 'INSERT INTO audit_log (action, user_id) VALUES (?, ?)', |
| 225 | 267 | ['login', userId] |
| 226 | 268 | ) |
| @@ -229,7 +271,7 @@ await db.transaction(async (trx) => { | ||
| 229 | 271 | await trx.updateTable('users').set({ last_login: new Date() }).where({ id: userId }) |
| 230 | 272 | |
| 231 | 273 | // Raw update |
| 232 | await trx.raw( | |
| 274 | await trx.unsafe( | |
| 233 | 275 | 'UPDATE statistics SET login_count = login_count + 1 WHERE user_id = ?', |
| 234 | 276 | [userId] |
| 235 | 277 | ) |
| @@ -256,7 +298,7 @@ console.log(explain) | ||
| 256 | 298 | |
| 257 | 299 | ```typescript |
| 258 | 300 | |
| 259 | import { createQueryBuilder, buildDatabaseSchema, buildSchemaMeta } from 'bun-query-builder' | |
| 301 | import { createQueryBuilder, buildDatabaseSchema, buildSchemaMeta, raw } from 'bun-query-builder' | |
| 260 | 302 | |
| 261 | 303 | const models = { |
| 262 | 304 | User: { |
| @@ -288,7 +330,7 @@ async function getComplexAnalytics() { | ||
| 288 | 330 | total_active: number |
| 289 | 331 | } |
| 290 | 332 | |
| 291 | const stats = await db.raw<MonthlyStats[]>(` | |
| 333 | const stats = await db.unsafe<MonthlyStats>(` | |
| 292 | 334 | WITH monthly_users AS ( |
| 293 | 335 | SELECT |
| 294 | 336 | strftime('%Y-%m', created_at) AS month, |
| @@ -310,15 +352,15 @@ async function getComplexAnalytics() { | ||
| 310 | 352 | // Combine with query builder |
| 311 | 353 | const basicStats = await db |
| 312 | 354 | .selectFrom('users') |
| 313 | .selectRaw(` | |
| 355 | .selectRaw(raw` | |
| 314 | 356 | COUNT(*) AS total, |
| 315 | 357 | COUNT(CASE WHEN active = 1 THEN 1 END) AS active, |
| 316 | 358 | AVG(age) AS avg_age |
| 317 | 359 | `) |
| 318 | 360 | .first() |
| 319 | 361 | |
| 320 | // Parameterized complex query | |
| 321 | const countryStats = await db.raw( | |
| 362 | // Parameterized complex query (`?` placeholders; use $1, $2, ... on Postgres) | |
| 363 | const countryStats = await db.unsafe( | |
| 322 | 364 | ` |
| 323 | 365 | SELECT |
| 324 | 366 | country, |
| @@ -200,11 +200,14 @@ const neverUpdated = await db | ||
| 200 | 200 | ## Raw Where Clauses |
| 201 | 201 | |
| 202 | 202 | ```typescript |
| 203 | import { raw } from 'bun-query-builder' | |
| 203 | 204 | |
| 204 | // Raw SQL in where | |
| 205 | // Raw SQL in where: one fragment, no bindings. The interpolated Date is | |
| 206 | // inlined as an escaped literal; use where() to bind request input. | |
| 207 | const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) | |
| 205 | 208 | const activeRecent = await db |
| 206 | 209 | .selectFrom('users') |
| 207 | .whereRaw('DATE(created_at) > DATE_SUB(NOW(), INTERVAL 30 DAY)') | |
| 210 | .whereRaw(raw`created_at > ${since}`) | |
| 208 | 211 | .get() |
| 209 | 212 | |
| 210 | 213 | ``` |
| @@ -7,7 +7,7 @@ There are two ways to use bun-query-builder: as a library and via the CLI. This | ||
| 7 | 7 | ### Quick Start |
| 8 | 8 | |
| 9 | 9 | ```ts |
| 10 | import { buildDatabaseSchema, buildSchemaMeta, createQueryBuilder } from 'bun-query-builder' | |
| 10 | import { buildDatabaseSchema, buildSchemaMeta, createQueryBuilder, raw } from 'bun-query-builder' | |
| 11 | 11 | |
| 12 | 12 | // Define your models with relationships |
| 13 | 13 | const models = { |
| @@ -86,7 +86,7 @@ const featuredProducts = await db | ||
| 86 | 86 | // Buddy's analytics: User engagement metrics |
| 87 | 87 | const userStats = await db |
| 88 | 88 | .selectFrom('users') |
| 89 | .selectRaw(db.sql` | |
| 89 | .selectRaw(raw` | |
| 90 | 90 | COUNT(posts.id) as post_count, |
| 91 | 91 | AVG(posts.engagement_score) as avg_engagement |
| 92 | 92 | `) |
| @@ -233,7 +233,7 @@ async function processOrderTransaction(orderData: any) { | ||
| 233 | 233 | |
| 234 | 234 | await tx |
| 235 | 235 | .updateTable('products') |
| 236 | .set({ stock_quantity: db.sql`stock_quantity - ${item.quantity}` }) | |
| 236 | .set({ stock_quantity: db.raw('stock_quantity - ?', [item.quantity]) }) | |
| 237 | 237 | .where({ id: item.product_id }) |
| 238 | 238 | .execute() |
| 239 | 239 | } |
| @@ -554,6 +554,10 @@ async function _clientUsage( | ||
| 554 | 554 | rows[0].seats |
| 555 | 555 | } |
| 556 | 556 | |
| 557 | // a bound fragment is accepted by the typed builder's where family too (#1146 follow-up) | |
| 558 | const typedRaw = null as unknown as import('../client').BoundSqlFragment | |
| 559 | users.where(typedRaw).orWhere(typedRaw) | |
| 560 | ||
| 557 | 561 | const email: string = await users.value('email') |
| 558 | 562 | const count: number = await users.value('login_count') |
| 559 | 563 | const names: string[] = await users.pluck('name') |
| @@ -1075,6 +1079,15 @@ async function _rawAndReturning( | ||
| 1075 | 1079 | // raw tagged-template with an escaped value |
| 1076 | 1080 | users.whereRaw(raw`name = ${'Ada'}`) |
| 1077 | 1081 | |
| 1082 | // db.raw(sql, bindings) builds a bound fragment that where() and set() accept | |
| 1083 | const dbRaw = null as unknown as import('../client').QueryBuilder<QDB>['raw'] | |
| 1084 | const bound: import('../client').BoundSqlFragment = dbRaw('login_count + ?', [1]) | |
| 1085 | users.where(bound) | |
| 1086 | users.orWhere(dbRaw('login_count > ?', [3])) | |
| 1087 | db.updateTable('users').set({ login_count: bound }).where(dbRaw('id = ?', [1])) | |
| 1088 | db.deleteFrom('users').where(dbRaw('id = ?', [1])) | |
| 1089 | dbRaw`SELECT 1` | |
| 1090 | ||
| 1078 | 1091 | // @ts-expect-error — a bare string is rejected by the SqlFragment type |
| 1079 | 1092 | users.whereRaw('age > 18') |
| 1080 | 1093 | // @ts-expect-error — the select builder's *Raw methods take no bindings (#1146) |
| @@ -30,6 +30,17 @@ function isRawExpression(expr: unknown): expr is RawExpression { | ||
| 30 | 30 | interface BoundSqlExpression { |
| 31 | 31 | sql: string |
| 32 | 32 | parameters?: readonly unknown[] |
| 33 | /** What the SQLite driver's `sql` tagged template carries its bindings in. */ | |
| 34 | values?: unknown | |
| 35 | } | |
| 36 | ||
| 37 | /** | |
| 38 | * A fragment with bound values, as `db.raw('stock - ?', [n])` builds it. | |
| 39 | * `set()` and `where()` render it at its position and bind its values. | |
| 40 | */ | |
| 41 | export interface BoundSqlFragment { | |
| 42 | readonly sql: string | |
| 43 | readonly parameters: readonly unknown[] | |
| 33 | 44 | } |
| 34 | 45 | |
| 35 | 46 | /** |
| @@ -69,6 +80,66 @@ function isBoundSqlExpression(expr: unknown): expr is BoundSqlExpression { | ||
| 69 | 80 | && (!('parameters' in expr) || Array.isArray((expr as BoundSqlExpression).parameters)) |
| 70 | 81 | } |
| 71 | 82 | |
| 83 | /** | |
| 84 | * `db.raw(sql, bindings)`: a `{ sql, parameters }` fragment. | |
| 85 | * | |
| 86 | * `db.raw` was a tagged-template passthrough only. Called the way the docs | |
| 87 | * called it, `db.raw('stock - ?', [quantity])`, it returned the SQLite driver's | |
| 88 | * identifier marker with the values dropped — `set()` then changed no rows — and | |
| 89 | * threw `Query not called as a tagged template literal` on Postgres. | |
| 90 | * | |
| 91 | * Awaiting the fragment rejects, because `await db.raw(sql, [..])` reads like a | |
| 92 | * query and would otherwise resolve to this object; `db.unsafe` runs SQL. | |
| 93 | */ | |
| 94 | function boundFragment(sql: string, args: readonly unknown[]): BoundSqlFragment { | |
| 95 | const [bindings, ...rest] = args | |
| 96 | if (rest.length > 0 || (bindings !== undefined && !Array.isArray(bindings))) { | |
| 97 | throw new TypeError( | |
| 98 | '[query-builder] db.raw(sql, bindings): pass the bindings as one array, db.raw(\'a = ? AND b = ?\', [a, b]).', | |
| 99 | ) | |
| 100 | } | |
| 101 | const fragment = { sql, parameters: [...(bindings ?? [])] } | |
| 102 | Object.defineProperty(fragment, 'then', { | |
| 103 | enumerable: false, | |
| 104 | value: (_onFulfilled: unknown, onRejected?: (reason: unknown) => unknown) => { | |
| 105 | const error = new TypeError( | |
| 106 | '[query-builder] db.raw(sql, bindings) builds a fragment for set() or where(), not a query. ' | |
| 107 | + 'Run SQL with db.unsafe(sql, params).', | |
| 108 | ) | |
| 109 | if (typeof onRejected === 'function') | |
| 110 | return onRejected(error) | |
| 111 | throw error | |
| 112 | }, | |
| 113 | }) | |
| 114 | return fragment | |
| 115 | } | |
| 116 | ||
| 117 | /** | |
| 118 | * A query object from Bun's network driver (`db.sql` on Postgres and MySQL). | |
| 119 | * It is a Promise subclass that exposes no SQL text, so it cannot be spliced | |
| 120 | * into a statement; see {@link refuseNativeQuery}. | |
| 121 | */ | |
| 122 | function isNativeDriverQuery(value: unknown): boolean { | |
| 123 | return value instanceof Promise | |
| 124 | && typeof (value as { execute?: unknown }).execute === 'function' | |
| 125 | && typeof (value as { cancel?: unknown }).cancel === 'function' | |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * Throw for a Bun network-driver query used as a fragment. | |
| 130 | * | |
| 131 | * `where(db.sql\`name = ${v}\`)` on Postgres read the query object as an empty | |
| 132 | * column map, added no condition, and returned every row. In `set()` the | |
| 133 | * driver rejected it with `Unknown object is not a valid PostgreSQL type`. | |
| 134 | */ | |
| 135 | function refuseNativeQuery(context: string): never { | |
| 136 | throw new TypeError( | |
| 137 | `[query-builder] ${context}: a Bun \`sql\`...\`\` query cannot be used as a fragment, because its SQL text ` | |
| 138 | + `is not readable. Bind values with db.raw('name = ?', [value]) or { sql, parameters }, or use the ` | |
| 139 | + `exported \`raw\` tagged template.`, | |
| 140 | ) | |
| 141 | } | |
| 142 | ||
| 72 | 143 | /** |
| 73 | 144 | * Render a parameterized SQL expression at its current position in a query. |
| 74 | 145 | * |
| @@ -77,7 +148,9 @@ function isBoundSqlExpression(expr: unknown): expr is BoundSqlExpression { | ||
| 77 | 148 | * parameters and start after the query parameters already collected. |
| 78 | 149 | */ |
| 79 | 150 | function renderBoundSqlExpression(expression: BoundSqlExpression, startIndex: number): { text: string, parameters: readonly unknown[] } { |
| 80 | const parameters = expression.parameters ?? [] | |
| 151 | // The SQLite driver's `db.sql` tagged template keeps its bindings in `values`. | |
| 152 | // Reading only `parameters` sent `n = n + ?` with nothing bound. | |
| 153 | const parameters = expression.parameters ?? (Array.isArray(expression.values) ? expression.values : []) | |
| 81 | 154 | if (config.dialect !== 'postgres' || parameters.length === 0) |
| 82 | 155 | return { text: expression.sql, parameters } |
| 83 | 156 | |
| @@ -459,7 +532,12 @@ function validateQualifiedIdentifier(value: unknown, context: string): void { | ||
| 459 | 532 | * execution. That now throws here instead. See stacksjs/bun-query-builder#1146. |
| 460 | 533 | */ |
| 461 | 534 | function renderRawFragment(fragment: unknown, context: string, extra: readonly unknown[] = []): string { |
| 462 | if (dropsTrailingArguments(fragment, extra)) { | |
| 535 | const carried = typeof fragment === 'object' && fragment !== null | |
| 536 | ? ((fragment as BoundSqlExpression).parameters ?? (fragment as BoundSqlExpression).values) | |
| 537 | : undefined | |
| 538 | // A `db.sql\`name = ${v}\`` or `db.raw('name = ?', [v])` fragment carries its | |
| 539 | // values with it, and rendering only its text dropped them just the same. | |
| 540 | if (dropsTrailingArguments(fragment, extra) || (Array.isArray(carried) && carried.length > 0)) { | |
| 463 | 541 | // The advice differs by clause: where()/orWhere() and the model builder's |
| 464 | 542 | // bound whereRaw/orWhereRaw can stand in for a WHERE fragment. Nothing typed |
| 465 | 543 | // stands in for the others — having()'s type only takes selected columns, |
| @@ -679,19 +757,21 @@ export interface WhereRaw { | ||
| 679 | 757 | } |
| 680 | 758 | |
| 681 | 759 | /** |
| 682 | * Brand for SQL fragments produced by Bun's `sql\`...\`` tagged-template | |
| 683 | * (or any equivalent helper). Typed as `object` so the *Raw methods | |
| 684 | * (`whereRaw`, `selectRaw`, `groupByRaw`, `havingRaw`, `orderByRaw`) | |
| 685 | * refuse to compile when passed a bare string — concatenated user | |
| 686 | * input (`whereRaw(\`status = '${req.body.s}'\`)`) was the canonical | |
| 687 | * SQL-injection vector flagged by the audit as Q-3. | |
| 760 | * Brand for SQL fragments, such as the exported `raw` helper builds. Typed as | |
| 761 | * `object` so the *Raw methods (`whereRaw`, `selectRaw`, `groupByRaw`, | |
| 762 | * `havingRaw`, `orderByRaw`) refuse to compile when passed a bare string — | |
| 763 | * concatenated user input (`whereRaw(\`status = '${req.body.s}'\`)`) was the | |
| 764 | * canonical SQL-injection vector flagged by the audit as Q-3. | |
| 688 | 765 | * |
| 689 | * Callers who legitimately need raw SQL use `sql\`...\`` which | |
| 690 | * separates the SQL fragment from parameter values: | |
| 766 | * Callers who need raw SQL use `raw`, which escapes interpolated values, or | |
| 767 | * bind them with `db.raw('...', [values])` in `where()` and `set()`. A Bun | |
| 768 | * `sql\`...\`` query is not a fragment: on Postgres and MySQL its text cannot be | |
| 769 | * read back, and the *Raw methods take no bindings. | |
| 691 | 770 | * |
| 692 | 771 | * ```ts |
| 693 | * import { sql } from 'bun' | |
| 694 | * db.selectFrom('users').whereRaw(sql\`lower(name) = lower(${input})\`) | |
| 772 | * import { raw } from 'bun-query-builder' | |
| 773 | * db.selectFrom('users').whereRaw(raw\`lower(name) = lower(${input})\`) | |
| 774 | * db.selectFrom('users').where(db.raw('lower(name) = lower(?)', [input])) | |
| 695 | 775 | * ``` |
| 696 | 776 | * |
| 697 | 777 | * The runtime guard in each *Raw method also rejects bare strings as |
| @@ -879,7 +959,7 @@ export type TypedSelectQueryBuilder< | ||
| 879 | 959 | ) => TypedSelectQueryBuilder<DB, TTable, TSelected, TJoined, `${TSql} WHERE ${K} = ?`>) & (<K extends keyof DB[TTable]['columns'] & string, OP extends WhereOperator>( |
| 880 | 960 | expr: [K, OP, WhereValue<DB[TTable]['columns'][K]>], |
| 881 | 961 | ) => TypedSelectQueryBuilder<DB, TTable, TSelected, TJoined, `${TSql} WHERE ${K} ${Uppercase<OP>} ${OP extends 'in' | 'not in' ? '(?)' : '?'}`>) & (( |
| 882 | expr: WhereExpression<DB[TTable]['columns']> | string, | |
| 962 | expr: WhereExpression<DB[TTable]['columns']> | string | BoundSqlFragment, | |
| 883 | 963 | op?: WhereOperator, |
| 884 | 964 | value?: WhereValue<DB[TTable]['columns'][keyof DB[TTable]['columns'] & string]>, |
| 885 | 965 | ) => TypedSelectQueryBuilder<DB, TTable, TSelected, TJoined, `${TSql} WHERE ${string}`>) |
| @@ -888,7 +968,7 @@ export type TypedSelectQueryBuilder< | ||
| 888 | 968 | ) => TypedSelectQueryBuilder<DB, TTable, TSelected, TJoined, `${TSql} AND ${K} = ?`>) & (<K extends keyof DB[TTable]['columns'] & string, OP extends WhereOperator>( |
| 889 | 969 | expr: [K, OP, WhereValue<DB[TTable]['columns'][K]>], |
| 890 | 970 | ) => TypedSelectQueryBuilder<DB, TTable, TSelected, TJoined, `${TSql} AND ${K} ${Uppercase<OP>} ${OP extends 'in' | 'not in' ? '(?)' : '?'}`>) & (( |
| 891 | expr: WhereExpression<DB[TTable]['columns']> | string, | |
| 971 | expr: WhereExpression<DB[TTable]['columns']> | string | BoundSqlFragment, | |
| 892 | 972 | op?: WhereOperator, |
| 893 | 973 | value?: WhereValue<DB[TTable]['columns'][keyof DB[TTable]['columns'] & string]>, |
| 894 | 974 | ) => TypedSelectQueryBuilder<DB, TTable, TSelected, TJoined, `${TSql} AND ${string}`>) |
| @@ -897,7 +977,7 @@ export type TypedSelectQueryBuilder< | ||
| 897 | 977 | ) => TypedSelectQueryBuilder<DB, TTable, TSelected, TJoined, `${TSql} OR ${K} = ?`>) & (<K extends keyof DB[TTable]['columns'] & string, OP extends WhereOperator>( |
| 898 | 978 | expr: [K, OP, WhereValue<DB[TTable]['columns'][K]>], |
| 899 | 979 | ) => TypedSelectQueryBuilder<DB, TTable, TSelected, TJoined, `${TSql} OR ${K} ${Uppercase<OP>} ${OP extends 'in' | 'not in' ? '(?)' : '?'}`>) & (( |
| 900 | expr: WhereExpression<DB[TTable]['columns']> | string, | |
| 980 | expr: WhereExpression<DB[TTable]['columns']> | string | BoundSqlFragment, | |
| 901 | 981 | op?: WhereOperator, |
| 902 | 982 | value?: WhereValue<DB[TTable]['columns'][keyof DB[TTable]['columns'] & string]>, |
| 903 | 983 | ) => TypedSelectQueryBuilder<DB, TTable, TSelected, TJoined, `${TSql} OR ${string}`>) |
renamed throughout · 10 hidden lines
| @@ -959,8 +1039,8 @@ export interface BaseSelectQueryBuilder< renamed throughout | ||
| 959 | 1039 | * |
| 960 | 1040 | * @example |
| 961 | 1041 | * ```ts |
| 962 | * const rows = await db.selectFrom('users').selectRaw(sql`count(*) as c`).get() | |
| 963 | * const sqlText = db.selectFrom('users').selectRaw(sql`now() as ts`).toSQL() | |
| 1042 | * const rows = await db.selectFrom('users').selectRaw(raw`count(*) as c`).get() | |
| 1043 | * const sqlText = db.selectFrom('users').selectRaw(raw`now() as ts`).toSQL() | |
| 964 | 1044 | * ``` |
| 965 | 1045 | */ |
| 966 | 1046 | selectRaw: (fragment: SqlFragment) => SelectQueryBuilder<DB, TTable, TSelected, TJoined> |
| @@ -977,7 +1057,7 @@ export interface BaseSelectQueryBuilder< | ||
| 977 | 1057 | * ``` |
| 978 | 1058 | */ |
| 979 | 1059 | where: <K extends keyof DB[TTable]['columns'] & string>( |
| 980 | expr: WhereExpression<DB[TTable]['columns']> | K, | |
| 1060 | expr: WhereExpression<DB[TTable]['columns']> | K | BoundSqlFragment, | |
| 981 | 1061 | op?: WhereOperator, |
| 982 | 1062 | value?: WhereValue<DB[TTable]['columns'][K]>, |
| 983 | 1063 | ) => SelectQueryBuilder<DB, TTable, TSelected, TJoined> |
| @@ -988,8 +1068,8 @@ export interface BaseSelectQueryBuilder< | ||
| 988 | 1068 | * |
| 989 | 1069 | * @example |
| 990 | 1070 | * ```ts |
| 991 | * const rows = await db.selectFrom('users').whereRaw(sql`lower(name) = lower(${ 'Alice' })`).get() | |
| 992 | * const sqlText = db.selectFrom('users').whereRaw(sql`custom_condition`).toSQL() | |
| 1071 | * const rows = await db.selectFrom('users').whereRaw(raw`lower(name) = lower(${'Alice'})`).get() | |
| 1072 | * const sqlText = db.selectFrom('users').whereRaw(raw`custom_condition`).toSQL() | |
| 993 | 1073 | * ``` |
| 994 | 1074 | */ |
| 995 | 1075 | whereRaw: (fragment: SqlFragment) => SelectQueryBuilder<DB, TTable, TSelected, TJoined> |
| @@ -1025,7 +1105,7 @@ export interface BaseSelectQueryBuilder< | ||
| 1025 | 1105 | * @example |
| 1026 | 1106 | * ```ts |
| 1027 | 1107 | * const rows = await db.selectFrom('users').whereIn('id', [1, 2, 3]).get() |
| 1028 | * const sqlText = db.selectFrom('users').whereIn('id', db.selectFrom('admins').selectRaw(sql`id`)).toSQL() | |
| 1108 | * const sqlText = db.selectFrom('users').whereIn('id', db.selectFrom('admins').selectRaw(raw`id`)).toSQL() | |
| 1029 | 1109 | * ``` |
| 1030 | 1110 | */ |
| 1031 | 1111 | whereIn: <K extends keyof DB[TTable]['columns'] & string>(column: K, values: DB[TTable]['columns'][K][] | { toSQL: () => string }) => SelectQueryBuilder<DB, TTable, TSelected, TJoined> |
| @@ -1286,7 +1366,7 @@ export interface BaseSelectQueryBuilder< | ||
| 1286 | 1366 | * ``` |
| 1287 | 1367 | */ |
| 1288 | 1368 | andWhere: <K extends keyof DB[TTable]['columns'] & string>( |
| 1289 | expr: WhereExpression<DB[TTable]['columns']> | K, | |
| 1369 | expr: WhereExpression<DB[TTable]['columns']> | K | BoundSqlFragment, | |
| 1290 | 1370 | op?: WhereOperator, |
| 1291 | 1371 | value?: WhereValue<DB[TTable]['columns'][K]>, |
| 1292 | 1372 | ) => SelectQueryBuilder<DB, TTable, TSelected, TJoined> |
| @@ -1302,7 +1382,7 @@ export interface BaseSelectQueryBuilder< | ||
| 1302 | 1382 | * ``` |
| 1303 | 1383 | */ |
| 1304 | 1384 | orWhere: <K extends keyof DB[TTable]['columns'] & string>( |
| 1305 | expr: WhereExpression<DB[TTable]['columns']> | K, | |
| 1385 | expr: WhereExpression<DB[TTable]['columns']> | K | BoundSqlFragment, | |
| 1306 | 1386 | op?: WhereOperator, |
| 1307 | 1387 | value?: WhereValue<DB[TTable]['columns'][K]>, |
| 1308 | 1388 | ) => SelectQueryBuilder<DB, TTable, TSelected, TJoined> |
renamed throughout · 10 hidden lines
| @@ -1466,8 +1546,8 @@ export interface BaseSelectQueryBuilder< renamed throughout | ||
| 1466 | 1546 | * |
| 1467 | 1547 | * @example |
| 1468 | 1548 | * ```ts |
| 1469 | * const sql = db.selectFrom('users').groupByRaw(sql`date_trunc('day', created_at)`).toSQL() | |
| 1470 | * const rows = await db.selectFrom('users').groupByRaw(sql`1`).get() | |
| 1549 | * const sql = db.selectFrom('users').groupByRaw(raw`date_trunc('day', created_at)`).toSQL() | |
| 1550 | * const rows = await db.selectFrom('users').groupByRaw(raw`1`).get() | |
| 1471 | 1551 | * ``` |
| 1472 | 1552 | */ |
| 1473 | 1553 | groupByRaw: (fragment: SqlFragment) => SelectQueryBuilder<DB, TTable, TSelected, TJoined> |
renamed throughout · 10 hidden lines
| @@ -1490,8 +1570,8 @@ export interface BaseSelectQueryBuilder< renamed throughout | ||
| 1490 | 1570 | * |
| 1491 | 1571 | * @example |
| 1492 | 1572 | * ```ts |
| 1493 | * const sql = db.selectFrom('users').groupBy('role').havingRaw(sql`count(*) > 10`).toSQL() | |
| 1494 | * const rows = await db.selectFrom('users').havingRaw(sql`count(*) > 0`).get() | |
| 1573 | * const sql = db.selectFrom('users').groupBy('role').havingRaw(raw`count(*) > 10`).toSQL() | |
| 1574 | * const rows = await db.selectFrom('users').havingRaw(raw`count(*) > 0`).get() | |
| 1495 | 1575 | * ``` |
| 1496 | 1576 | */ |
| 1497 | 1577 | havingRaw: (fragment: SqlFragment) => SelectQueryBuilder<DB, TTable, TSelected, TJoined> |
renamed throughout · 10 hidden lines
| @@ -1527,8 +1607,8 @@ export interface BaseSelectQueryBuilder< renamed throughout | ||
| 1527 | 1607 | * |
| 1528 | 1608 | * @example |
| 1529 | 1609 | * ```ts |
| 1530 | * const sql = db.selectFrom('users').orderByRaw(sql`random()`).toSQL() | |
| 1531 | * const rows = await db.selectFrom('users').orderByRaw(sql`1`).get() | |
| 1610 | * const sql = db.selectFrom('users').orderByRaw(raw`random()`).toSQL() | |
| 1611 | * const rows = await db.selectFrom('users').orderByRaw(raw`1`).get() | |
| 1532 | 1612 | * ``` |
| 1533 | 1613 | */ |
| 1534 | 1614 | orderByRaw: (fragment: SqlFragment) => SelectQueryBuilder<DB, TTable, TSelected, TJoined> |
| @@ -2324,7 +2404,7 @@ export interface DeleteQueryBuilder<DB extends AnyDatabaseSchema, TTable extends | ||
| 2324 | 2404 | * an injected operator is unrecoverable. |
| 2325 | 2405 | */ |
| 2326 | 2406 | where: <K extends keyof DB[TTable]['columns'] & string>( |
| 2327 | expr: WhereExpression<DB[TTable]['columns']> | K, | |
| 2407 | expr: WhereExpression<DB[TTable]['columns']> | K | BoundSqlFragment, | |
| 2328 | 2408 | op?: WhereOperator, |
| 2329 | 2409 | value?: WhereValue<DB[TTable]['columns'][K]>, |
| 2330 | 2410 | ) => DeleteQueryBuilder<DB, TTable> |
| @@ -2544,14 +2624,20 @@ export interface QueryBuilder<DB extends AnyDatabaseSchema> { | ||
| 2544 | 2624 | /** |
| 2545 | 2625 | * # `raw` |
| 2546 | 2626 | * |
| 2547 | * Tagged template passthrough to Bun.sql. | |
| 2627 | * Called with a string, builds a bound fragment for `set()` and `where()`: | |
| 2628 | * each `?` binds the matching value, on every dialect. It is not a query — | |
| 2629 | * run one with `db.unsafe(sql, params)`. | |
| 2630 | * | |
| 2631 | * As a tagged template it passes through to the driver's `sql` tag. | |
| 2548 | 2632 | * |
| 2549 | 2633 | * @example |
| 2550 | 2634 | * ```ts |
| 2635 | * await db.updateTable('products').set({ stock: db.raw('stock - ?', [quantity]) }).where('id', '=', id).execute() | |
| 2551 | 2636 | * const q = db.raw`SELECT ${1} as one` |
| 2552 | 2637 | * ``` |
| 2553 | 2638 | */ |
| 2554 | raw: (strings: TemplateStringsArray, ...values: unknown[]) => SqlFragment | |
| 2639 | // eslint-disable-next-line pickier/no-unused-vars | |
| 2640 | raw: ((sql: string, bindings?: readonly unknown[]) => BoundSqlFragment) & ((strings: TemplateStringsArray, ...values: unknown[]) => SqlFragment) | |
| 2555 | 2641 | /** |
| 2556 | 2642 | * # `simple` |
| 2557 | 2643 | * |
| @@ -3793,6 +3879,16 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial | ||
| 3793 | 3879 | return self |
| 3794 | 3880 | } |
| 3795 | 3881 | |
| 3882 | // A bound fragment, handled before the object branch as in where(). | |
| 3883 | if (isNativeDriverQuery(expr)) | |
| 3884 | refuseNativeQuery(`${label}(expr)`) | |
| 3885 | if (isBoundSqlExpression(expr)) { | |
| 3886 | const rendered = renderBoundSqlExpression(expr, whereParams.length + 1) | |
| 3887 | whereParams.push(...rendered.parameters) | |
| 3888 | pushWhere(conn, rendered.text) | |
| 3889 | return self | |
| 3890 | } | |
| 3891 | ||
| 3796 | 3892 | // Object format: { name: 'Alice', age: 25 } |
| 3797 | 3893 | if (expr && typeof expr === 'object' && !('raw' in expr)) { |
| 3798 | 3894 | const conditions: string[] = [] |
| @@ -5195,6 +5291,19 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial | ||
| 5195 | 5291 | return this |
| 5196 | 5292 | } |
| 5197 | 5293 | |
| 5294 | // A bound fragment — db.raw('name = ?', [v]), { sql, parameters }, or the | |
| 5295 | // SQLite driver's db.sql`...` — is an object too. Read as a column map it | |
| 5296 | // became `sql = ?` on SQLite, and a Postgres query object has no keys, so | |
| 5297 | // it added no condition and every row came back. | |
| 5298 | if (isNativeDriverQuery(expr)) | |
| 5299 | refuseNativeQuery('where(expr)') | |
| 5300 | if (isBoundSqlExpression(expr)) { | |
| 5301 | const rendered = renderBoundSqlExpression(expr, whereParams.length + 1) | |
| 5302 | whereParams.push(...rendered.parameters) | |
| 5303 | pushWhere('AND', rendered.text) | |
| 5304 | return this | |
| 5305 | } | |
| 5306 | ||
| 5198 | 5307 | // Handle object format: { name: 'Alice', age: 25 } |
| 5199 | 5308 | if (expr && typeof expr === 'object' && !isRawExpression(expr)) { |
| 5200 | 5309 | const whereObject = expr as Record<string, unknown> |
| @@ -7534,6 +7643,8 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial | ||
| 7534 | 7643 | for (let i = 0; i < len; i++) { |
| 7535 | 7644 | const key = keys[i] |
| 7536 | 7645 | const value = (shaped as any)[key] |
| 7646 | if (isNativeDriverQuery(value)) | |
| 7647 | refuseNativeQuery(`set(${key})`) | |
| 7537 | 7648 | if (isRawExpression(value)) { |
| 7538 | 7649 | setClauses[i] = `${quoteId(key)} = ${value.raw}` |
| 7539 | 7650 | } |
| @@ -7619,6 +7730,8 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial | ||
| 7619 | 7730 | // as success. Handled before the object branch because a bound |
| 7620 | 7731 | // expression is an object and would otherwise be read as the column |
| 7621 | 7732 | // map `{ sql: ... }`. See #1101. |
| 7733 | if (isNativeDriverQuery(expr)) | |
| 7734 | refuseNativeQuery('updateTable.where(expr)') | |
| 7622 | 7735 | if (isRawExpression(expr)) { |
| 7623 | 7736 | appendPredicate(expr.raw) |
| 7624 | 7737 | built = prepareQuery(sqlText, params) |
| @@ -7917,6 +8030,8 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial | ||
| 7917 | 8030 | // fragment was interpolated as a bound value, so Postgres rejected |
| 7918 | 8031 | // `where(raw('id = 1'))` with "invalid input syntax for type |
| 7919 | 8032 | // boolean". The signature accepts SqlFragment, so it has to work. |
| 8033 | if (isNativeDriverQuery(expr)) | |
| 8034 | refuseNativeQuery('deleteFrom.where(expr)') | |
| 7920 | 8035 | if (isRawExpression(expr)) { |
| 7921 | 8036 | appendPredicate(expr.raw) |
| 7922 | 8037 | built = null |
| @@ -8110,7 +8225,10 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial | ||
| 8110 | 8225 | } |
| 8111 | 8226 | }, |
| 8112 | 8227 | sql: _sql, |
| 8113 | raw(strings: TemplateStringsArray, ...values: any[]) { | |
| 8228 | // One implementation behind the two call signatures declared on QueryBuilder. | |
| 8229 | raw(strings: TemplateStringsArray | string, ...values: any[]): any { | |
| 8230 | if (typeof strings === 'string') | |
| 8231 | return boundFragment(strings, values) | |
| 8114 | 8232 | return _sql(strings, ...values) |
| 8115 | 8233 | }, |
| 8116 | 8234 | simple(strings: TemplateStringsArray, ...values: any[]) { |
| @@ -0,0 +1,151 @@ | ||
| 1 | /** | |
| 2 | * `db.sql` and `db.raw` fragments carrying values. | |
| 3 | * | |
| 4 | * The docs used both as fragments, and neither kept its values: | |
| 5 | * | |
| 6 | * - `db.raw('stock - ?', [quantity])` returned the SQLite driver's identifier | |
| 7 | * marker with the array dropped, so `set({ stock: ... })` changed no rows. On | |
| 8 | * Postgres it threw `Query not called as a tagged template literal`. | |
| 9 | * - `where(db.sql\`name = ${v}\`)` on Postgres read the query object as an empty | |
| 10 | * column map, added no condition, and returned EVERY row. On SQLite it threw. | |
| 11 | * - `whereRaw(db.sql\`name = ${v}\`)` on SQLite rendered `name = ?` with the value | |
| 12 | * dropped and returned [] without an error; `orderByRaw(db.sql\`${col}\`)` | |
| 13 | * silently ordered by a constant. | |
| 14 | * - `set({ n: db.sql\`n + ${1}\` })` on SQLite bound nothing to the `?`. | |
| 15 | * - `where({ sql, parameters })` was read as a column named `sql`. | |
| 16 | * | |
| 17 | * Each now binds its values or throws. Runs each dialect in a subprocess | |
| 18 | * because the connection config is global; Postgres is skipped without a server. | |
| 19 | */ | |
| 20 | ||
| 21 | import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' | |
| 22 | import { tmpdir } from 'node:os' | |
| 23 | import { join, resolve } from 'node:path' | |
| 24 | import process from 'node:process' | |
| 25 | import { describe, expect, it } from 'bun:test' | |
| 26 | import { PG_URL, probePostgres } from './pg' | |
| 27 | ||
| 28 | const pgAvailable = await probePostgres() | |
| 29 | ||
| 30 | function runProbe(dialect: 'sqlite' | 'postgres'): { code: number | null, out: string, err: string } { | |
| 31 | const srcEntry = resolve(import.meta.dir, '../src/index.ts') | |
| 32 | const dir = mkdtempSync(join(tmpdir(), 'qb-boundfrag-')) | |
| 33 | const scriptPath = join(dir, 'probe.ts') | |
| 34 | ||
| 35 | writeFileSync(scriptPath, ` | |
| 36 | import { SQL } from 'bun' | |
| 37 | import { setConfig, resetConnection, createQueryBuilder, buildDatabaseSchema, buildSchemaMeta } from ${JSON.stringify(srcEntry)} | |
| 38 | ||
| 39 | const DIALECT = ${JSON.stringify(dialect)} | |
| 40 | const PG = ${JSON.stringify(PG_URL)} | |
| 41 | const failures = [] | |
| 42 | const check = (label, got, want) => { | |
| 43 | const g = JSON.stringify(got), w = JSON.stringify(want) | |
| 44 | if (g !== w) failures.push(label + ': got ' + g + ', want ' + w) | |
| 45 | } | |
| 46 | const throws = async (label, fn, pattern) => { | |
| 47 | try { await fn(); failures.push(label + ': did not throw') } | |
| 48 | catch (e) { if (!pattern.test(String(e && e.message))) failures.push(label + ': threw ' + (e && e.message)) } | |
| 49 | } | |
| 50 | ||
| 51 | if (DIALECT === 'postgres') { | |
| 52 | const r = new SQL(PG) | |
| 53 | await r.unsafe('DROP TABLE IF EXISTS _qb_boundfrag') | |
| 54 | await r.unsafe('CREATE TABLE _qb_boundfrag (id int primary key, name text, n int)') | |
| 55 | await r.end() | |
| 56 | setConfig({ dialect: 'postgres', database: { url: PG } }) | |
| 57 | } | |
| 58 | else { | |
| 59 | setConfig({ dialect: 'sqlite', database: { database: ${JSON.stringify(join(dir, 'probe.db'))} } }) | |
| 60 | } | |
| 61 | resetConnection() | |
| 62 | ||
| 63 | const models = { _qb_boundfrag: { columns: { id: { type: 'integer', isPrimaryKey: true }, name: { type: 'text' }, n: { type: 'integer' } } } } | |
| 64 | const db = createQueryBuilder({ schema: buildDatabaseSchema(models), meta: buildSchemaMeta(models), autoMigration: { enabled: false } }) | |
| 65 | if (DIALECT === 'sqlite') | |
| 66 | await db.unsafe('CREATE TABLE _qb_boundfrag (id integer primary key, name text, n integer)') | |
| 67 | ||
| 68 | const reset = async () => { | |
| 69 | await db.unsafe('DELETE FROM _qb_boundfrag') | |
| 70 | await db.unsafe("INSERT INTO _qb_boundfrag (id, name, n) VALUES (1, 'a', 10), (2, 'b', 20)") | |
| 71 | } | |
| 72 | const ns = async () => (await db.unsafe('SELECT n FROM _qb_boundfrag ORDER BY id')).map(r => Number(r.n)) | |
| 73 | const ids = rows => rows.map(r => Number(r.id)).sort((a, b) => a - b) | |
| 74 | ||
| 75 | // db.raw(sql, bindings) in set() and where() | |
| 76 | await reset() | |
| 77 | await db.updateTable('_qb_boundfrag').set({ n: db.raw('n - ?', [3]) }).where('id', '=', 1).execute() | |
| 78 | check('set db.raw with binding', await ns(), [7, 20]) | |
| 79 | await db.updateTable('_qb_boundfrag').set({ n: db.raw('n + 1') }).where('id', '=', 2).execute() | |
| 80 | check('set db.raw without binding', await ns(), [7, 21]) | |
| 81 | check('where db.raw', ids(await db.selectFrom('_qb_boundfrag').where(db.raw('name = ?', ['b'])).get()), [2]) | |
| 82 | check('orWhere db.raw', ids(await db.selectFrom('_qb_boundfrag').where('id', '=', 1).orWhere(db.raw('n > ?', [20])).get()), [1, 2]) | |
| 83 | check('where db.raw beside where', ids(await db.selectFrom('_qb_boundfrag').where('id', '>', 0).where(db.raw('name = ?', ['a'])).get()), [1]) | |
| 84 | check('delete where db.raw', await (async () => { | |
| 85 | await db.deleteFrom('_qb_boundfrag').where(db.raw('n > ?', [20])).execute() | |
| 86 | return ids(await db.selectFrom('_qb_boundfrag').get()) | |
| 87 | })(), [1]) | |
| 88 | ||
| 89 | // { sql, parameters } in the select builder's where() | |
| 90 | await reset() | |
| 91 | check('where { sql, parameters }', ids(await db.selectFrom('_qb_boundfrag').where({ sql: 'name = ?', parameters: ['a'] }).get()), [1]) | |
| 92 | ||
| 93 | // db.raw is not a query | |
| 94 | await throws('await db.raw', () => db.raw('SELECT 1 WHERE 1 = ?', [1]), /not a query/) | |
| 95 | await throws('db.raw bindings not an array', async () => db.raw('a = ?', 1), /one array/) | |
| 96 | ||
| 97 | // *Raw methods refuse fragments that carry values | |
| 98 | await throws('whereRaw db.raw', async () => db.selectFrom('_qb_boundfrag').whereRaw(db.raw('name = ?', ['a'])), /no bindings/) | |
| 99 | ||
| 100 | if (DIALECT === 'sqlite') { | |
| 101 | // The SQLite driver's db.sql fragments carry readable text and values. | |
| 102 | await reset() | |
| 103 | await db.updateTable('_qb_boundfrag').set({ n: db.sql\`n + \${5}\` }).where('id', '=', 1).execute() | |
| 104 | check('set db.sql', await ns(), [15, 20]) | |
| 105 | check('where db.sql', ids(await db.selectFrom('_qb_boundfrag').where(db.sql\`name = \${'b'}\`).get()), [2]) | |
| 106 | await throws('whereRaw db.sql with value', async () => db.selectFrom('_qb_boundfrag').whereRaw(db.sql\`name = \${'a'}\`), /no bindings/) | |
| 107 | await throws('orderByRaw db.sql with value', async () => db.selectFrom('_qb_boundfrag').orderByRaw(db.sql\`\${'n'} desc\`), /no bindings/) | |
| 108 | check('selectRaw db.sql without value', (await db.selectFrom('_qb_boundfrag').selectRaw(db.sql\`count(*) as c\`).get()).length > 0, true) | |
| 109 | } | |
| 110 | else { | |
| 111 | // Bun's Postgres query objects expose no SQL text. | |
| 112 | await reset() | |
| 113 | await throws('where db.sql', async () => db.selectFrom('_qb_boundfrag').where(db.sql\`name = \${'a'}\`), /cannot be used as a fragment/) | |
| 114 | await throws('orWhere db.sql', async () => db.selectFrom('_qb_boundfrag').where('id', '=', 1).orWhere(db.sql\`name = \${'b'}\`), /cannot be used as a fragment/) | |
| 115 | await throws('set db.sql', async () => db.updateTable('_qb_boundfrag').set({ n: db.sql\`n + \${1}\` }), /cannot be used as a fragment/) | |
| 116 | await throws('update where db.sql', async () => db.updateTable('_qb_boundfrag').set({ n: 1 }).where(db.sql\`id = \${1}\`), /cannot be used as a fragment/) | |
| 117 | await throws('delete where db.sql', async () => db.deleteFrom('_qb_boundfrag').where(db.sql\`id = \${1}\`), /cannot be used as a fragment/) | |
| 118 | check('no rows changed', await ns(), [10, 20]) | |
| 119 | const r = new SQL(PG) | |
| 120 | await r.unsafe('DROP TABLE IF EXISTS _qb_boundfrag') | |
| 121 | await r.end() | |
| 122 | } | |
| 123 | ||
| 124 | if (failures.length) { | |
| 125 | console.error('FAILURES:\\n' + failures.join('\\n')) | |
| 126 | process.exit(1) | |
| 127 | } | |
| 128 | console.log('OK') | |
| 129 | process.exit(0) | |
| 130 | `) | |
| 131 | ||
| 132 | const proc = Bun.spawnSync({ cmd: ['bun', scriptPath], stdout: 'pipe', stderr: 'pipe', cwd: process.cwd(), env: { ...process.env } }) | |
| 133 | const dec = new TextDecoder() | |
| 134 | const result = { code: proc.exitCode, out: dec.decode(proc.stdout).trim(), err: dec.decode(proc.stderr).trim() } | |
| 135 | rmSync(dir, { recursive: true, force: true }) | |
| 136 | return result | |
| 137 | } | |
| 138 | ||
| 139 | describe('db.sql and db.raw fragments carrying values', () => { | |
| 140 | it('bind or refuse on SQLite', () => { | |
| 141 | const { code, out, err } = runProbe('sqlite') | |
| 142 | expect(code, `sqlite probe failed.\nstdout: ${out}\nstderr: ${err}`).toBe(0) | |
| 143 | expect(out).toContain('OK') | |
| 144 | }, 30_000) | |
| 145 | ||
| 146 | it.skipIf(!pgAvailable)('bind or refuse on Postgres', () => { | |
| 147 | const { code, out, err } = runProbe('postgres') | |
| 148 | expect(code, `postgres probe failed.\nstdout: ${out}\nstderr: ${err}`).toBe(0) | |
| 149 | expect(out).toContain('OK') | |
| 150 | }, 30_000) | |
| 151 | }) | |