also looking at this
fix(client): unsafe() refuses ?-style bindings on Postgres
#1172
3 files
+247
-13
| @@ -96,6 +96,29 @@ const result = await db.unsafe(` | ||
| 96 | 96 | Always prefer parameterized queries when possible to prevent SQL injection. Only use `unsafe` when absolutely necessary and never with user-provided input. |
| 97 | 97 | ::: |
| 98 | 98 | |
| 99 | ### Placeholder style is the connection's, not the builder's | |
| 100 | ||
| 101 | When you pass bindings to `unsafe`, you are writing for the driver directly, so the | |
| 102 | placeholders have to be the ones that connection understands. Postgres numbers its | |
| 103 | parameters; SQLite and MySQL take `?`: | |
| 104 | ||
| 105 | ```typescript | |
| 106 | // Postgres | |
| 107 | await db.unsafe('SELECT * FROM users WHERE email = $1 LIMIT 1', [email]) | |
| 108 | ||
| 109 | // SQLite and MySQL | |
| 110 | await db.unsafe('SELECT * FROM users WHERE email = ? LIMIT 1', [email]) | |
| 111 | ``` | |
| 112 | ||
| 113 | Postgres has no `?` parameter — it reads the token as an operator and rejects the | |
| 114 | word that follows, so the error names whatever comes next (`LIMIT`, above) rather | |
| 115 | than the placeholder. Because the same call works on SQLite and MySQL, this shows | |
| 116 | up as a one-dialect failure. `unsafe` therefore refuses bindings on Postgres when | |
| 117 | the query has no `$n`, and names the real problem at the call site. | |
| 118 | ||
| 119 | A query with no bindings is never inspected, so raw DDL and a literal `?` inside a | |
| 120 | string are unaffected. | |
| 121 | ||
| 99 | 122 | ## Execute Raw SQL |
| 100 | 123 | |
| 101 | 124 | Execute non-query SQL statements: |
| @@ -235,24 +258,31 @@ stats.forEach((s) => { | ||
| 235 | 258 | |
| 236 | 259 | ``` |
| 237 | 260 | |
| 238 | ## Prepared Statements | |
| 261 | ## Repeated Queries | |
| 239 | 262 | |
| 240 | Use prepared statements for repeated queries: | |
| 263 | `unsafe` is not a prepared-statement handle. It returns a query that runs when you | |
| 264 | await it and resolves to an array of rows, so there is no `.get()` to call per | |
| 265 | execution and nothing to finalize: | |
| 241 | 266 | |
| 242 | 267 | ```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. | |
| 270 | const user1 = await db.unsafe('SELECT * FROM users WHERE id = $1', [1]) | |
| 271 | const user2 = await db.unsafe('SELECT * FROM users WHERE id = $1', [2]) | |
| 243 | 272 | |
| 244 | // Prepare a statement | |
| 245 | const stmt = db.unsafe('SELECT * FROM users WHERE id = ?') | |
| 246 | ||
| 247 | // Execute multiple times efficiently | |
| 248 | const user1 = await stmt.get([1]) | |
| 249 | const user2 = await stmt.get([2]) | |
| 250 | const user3 = await stmt.get([3]) | |
| 273 | // Each resolves to an array of rows — index in, or use the builder's first(). | |
| 274 | console.log(user1[0]?.name) | |
| 275 | ``` | |
| 251 | 276 | |
| 252 | // Finalize when done | |
| 253 | stmt.finalize() | |
| 277 | Keep the SQL text identical across those calls and vary only the bindings. That is | |
| 278 | what lets the driver reuse its plan, and it is also what keeps the values | |
| 279 | parameterized rather than concatenated in. | |
| 254 | 280 | |
| 255 | ``` | |
| 281 | ::: tip | |
| 282 | The object `unsafe` returns before you await it is the driver's own, and its shape | |
| 283 | differs between `bun:sqlite` and `Bun.sql`. Await it and work with the rows; don't | |
| 284 | reach for methods on the unawaited value. | |
| 285 | ::: | |
| 256 | 286 | |
| 257 | 287 | ## Transaction with Raw Queries |
| 258 | 288 | |