ReviewOS

also looking at this

stacks/bun-query-builder

fix(client): unsafe() refuses ?-style bindings on Postgres

#1172
Merged glennmichael123 wants to merge fix/unsafe-placeholder-style into main
3 files +247 -13
docs/raw-queries.mdmodified+42-12
Changes to docs/raw-queries.md
@@ -96,6 +96,29 @@ const result = await db.unsafe(`
9696Always prefer parameterized queries when possible to prevent SQL injection. Only use `unsafe` when absolutely necessary and never with user-provided input.
9797:::
9898
99### Placeholder style is the connection's, not the builder's
100
101When you pass bindings to `unsafe`, you are writing for the driver directly, so the
102placeholders have to be the ones that connection understands. Postgres numbers its
103parameters; SQLite and MySQL take `?`:
104
105```typescript
106// Postgres
107await db.unsafe('SELECT * FROM users WHERE email = $1 LIMIT 1', [email])
108
109// SQLite and MySQL
110await db.unsafe('SELECT * FROM users WHERE email = ? LIMIT 1', [email])
111```
112
113Postgres has no `?` parameter — it reads the token as an operator and rejects the
114word that follows, so the error names whatever comes next (`LIMIT`, above) rather
115than the placeholder. Because the same call works on SQLite and MySQL, this shows
116up as a one-dialect failure. `unsafe` therefore refuses bindings on Postgres when
117the query has no `$n`, and names the real problem at the call site.
118
119A query with no bindings is never inspected, so raw DDL and a literal `?` inside a
120string are unaffected.
121
99122## Execute Raw SQL
100123
101124Execute non-query SQL statements:
@@ -235,24 +258,31 @@ stats.forEach((s) => {
235258
236259```
237260
238## Prepared Statements
261## Repeated Queries
239262
240Use prepared statements for repeated queries:
263`unsafe` is not a prepared-statement handle. It returns a query that runs when you
264await it and resolves to an array of rows, so there is no `.get()` to call per
265execution and nothing to finalize:
241266
242267```typescript
268// Run it once per set of values, awaiting each. Postgres placeholders shown —
269// see "Placeholder style is the connection's, not the builder's" above.
270const user1 = await db.unsafe('SELECT * FROM users WHERE id = $1', [1])
271const user2 = await db.unsafe('SELECT * FROM users WHERE id = $1', [2])
243272
244// Prepare a statement
245const stmt = db.unsafe('SELECT * FROM users WHERE id = ?')
246
247// Execute multiple times efficiently
248const user1 = await stmt.get([1])
249const user2 = await stmt.get([2])
250const user3 = await stmt.get([3])
273// Each resolves to an array of rows — index in, or use the builder's first().
274console.log(user1[0]?.name)
275```
251276
252// Finalize when done
253stmt.finalize()
277Keep the SQL text identical across those calls and vary only the bindings. That is
278what lets the driver reuse its plan, and it is also what keeps the values
279parameterized rather than concatenated in.
254280
255```
281::: tip
282The object `unsafe` returns before you await it is the driver's own, and its shape
283differs between `bun:sqlite` and `Bun.sql`. Await it and work with the rows; don't
284reach for methods on the unawaited value.
285:::
256286
257287## Transaction with Raw Queries
258288