ReviewOS

also looking at this

stacks/bun-query-builder

fix(client)!: bind db.raw and db.sql fragment values or refuse them

#1152
Merged glennmichael123 wants to merge fix/db-sql-raw-bindings into main
16 files +455 -118
docs/features/builder.mdmodified+19-17
Changes to docs/features/builder.md
@@ -48,6 +48,8 @@ const recentOrders = await db
4848## Selecting Data
4949
5050```ts
51import { raw } from 'bun-query-builder'
52
5153// Select all columns from projects
5254const allProjects = await db
5355 .selectFrom('projects')
@@ -64,7 +66,7 @@ const userInfo = await db
6466const userStats = await db
6567 .selectFrom('users')
6668 .select('users', 'id', 'name')
67 .selectRaw(db.sql`COUNT(posts.id) as post_count`)
69 .selectRaw(raw`COUNT(posts.id) as post_count`)
6870 .leftJoin('posts', 'posts.user_id', '=', 'users.id')
6971 .groupBy('users.id', 'users.name')
7072 .execute()
@@ -74,7 +76,7 @@ const userProfiles = await db
7476 .selectFrom('users')
7577 .innerJoin('profiles', 'profiles.user_id', '=', 'users.id')
7678 .select('users', 'id', 'name')
77 .selectRaw(db.sql`profiles.bio, profiles.avatar_url`)
79 .selectRaw(raw`profiles.bio, profiles.avatar_url`)
7880 .where({ 'users.active': true })
7981 .execute()
8082```
@@ -82,7 +84,7 @@ const userProfiles = await db
8284### Aliases and Expressions
8385
8486- 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
8688- Combine `select()` and `selectRaw()` calls to build comprehensive column lists
8789- Always prefix column names with table names when joining to avoid ambiguity
8890
@@ -375,9 +377,9 @@ Use raw sparingly for complex cases not covered by helpers.
375377```ts
376378await db
377379 .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`)
381383 .execute()
382384```
383385
@@ -684,7 +686,7 @@ await db
684686// Update with complex conditions
685687await db
686688 .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') })
688690 .where({ email: 'chris@example.com' })
689691 .andWhere(['active', '=', true])
690692 .execute()
@@ -869,7 +871,7 @@ const users = await db
869871// Avoid: Excessive raw SQL
870872const users = await db
871873 .selectFrom('users')
872 .whereRaw(db.sql`active = true ${includeAdmin ? 'OR role = "admin"' : ''}`)
874 .whereRaw(raw(`active = true${includeAdmin ? " OR role = 'admin'" : ''}`))
873875 .execute()
874876```
875877
@@ -917,7 +919,7 @@ const user = await db
917919// Dangerous: String interpolation
918920const user = await db
919921 .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
921923```
922924
923925### Error Handling and Monitoring
renamed throughout · 16 hidden lines
Changes to docs/features/builder.md
@@ -1071,14 +1073,14 @@ const searchQuery = 'TypeScript' renamed throughout
10711073const articleResults = db
10721074 .selectFrom('articles')
10731075 .select('articles', 'id', 'title', 'created_at')
1074 .selectRaw(db.sql`'article' as content_type`)
1076 .selectRaw(raw`'article' as content_type`)
10751077 .where(['title', 'like', `%${searchQuery}%`])
10761078 .orWhere(['content', 'like', `%${searchQuery}%`])
10771079
10781080const projectResults = db
10791081 .selectFrom('projects')
10801082 .select('projects', 'id', 'name as title', 'created_at')
1081 .selectRaw(db.sql`'project' as content_type`)
1083 .selectRaw(raw`'project' as content_type`)
10821084 .where(['name', 'like', `%${searchQuery}%`])
10831085 .orWhere(['description', 'like', `%${searchQuery}%`])
10841086
Changes to docs/features/builder.md
@@ -1099,7 +1101,7 @@ const userStats = await db
10991101 .withCount('posts', 'total_posts')
11001102 .withCount('posts', 'published_posts', ['published', '=', true])
11011103 .withCount('comments', 'total_comments')
1102 .selectRaw(db.sql`
1104 .selectRaw(raw`
11031105 CASE
11041106 WHEN COUNT(posts.id) > 10 THEN 'high'
11051107 WHEN COUNT(posts.id) > 5 THEN 'medium'
@@ -1114,13 +1116,13 @@ const userStats = await db
11141116// Monthly user registration trends
11151117const registrationTrends = await db
11161118 .selectFrom('users')
1117 .selectRaw(db.sql`
1119 .selectRaw(raw`
11181120 DATE_TRUNC('month', created_at) as month,
1119 COUNT(_) as new_users,
1121 COUNT(*) as new_users,
11201122 COUNT(CASE WHEN role = 'admin' THEN 1 END) as new_admins
11211123 `)
11221124 .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)`)
11241126 .orderBy('month', 'desc')
11251127 .execute()
11261128```
@@ -1144,7 +1146,7 @@ const chrisTeamWithStats = await db
11441146const projectsWithCollaborators = await db
11451147 .selectFrom('projects')
11461148 .select('projects', 'id', 'name', 'description')
1147 .selectRaw(db.sql`
1149 .selectRaw(raw`
11481150 STRING_AGG(users.name, ', ') as collaborators,
11491151 COUNT(DISTINCT project_users.user_id) as collaborator_count
11501152 `)
@@ -1226,7 +1228,7 @@ Use `whereColumn(left, op, right)`.
12261228
12271229### How do I add arbitrary fragments
12281230
1229Use `selectRaw`, `whereRaw`, `groupByRaw`, or `havingRaw` and pass a Bun `sql` fragment.
1231Use `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.
12301232
12311233---
12321234