Closes stacksjs/bun-query-builder#1146.
The array was stored as one binding
Model.query().whereRaw(fragment, ...params) stored its rest array unchanged, so the Knex/Laravel call whereRaw(sql, [value]) stored rawParams = [[value]]. buildWhereClauses spreads one level, which left the array as a single binding. On SQLite:
- The array is the only value of a read.
.all([value])takes a lone array as the whole bindings list, so it worked by accident. - Another clause binds a value. It threw
expected 2 values, received 1when the array came first, andBinding expected string, …when it did not. - Any write.
SqliteExecutor.runpasses params unspread, sodelete(),update()andincrement()threw even with a single value.
whereRaw('1 = 1', []).where('country', 'US') was worse: country = ? was left unbound, SQLite read it as NULL, and the query returned no rows with no error. On Postgres a one-value array happened to pass; two values failed with insufficient data left in message.
resolveRawBindings now reads a lone array as the bindings list, so whereRaw(sql, a, b) and whereRaw(sql, [a, b]) build the same query. The array is copied at the call. A lone undefined on a fragment with no ? counts as no bindings. That is what (sql, bindings?) => q.whereRaw(sql, bindings) forwards, and it failed the same way. With a ? present it still binds NULL.
The docs taught it on a builder with no bindings
docs/guide/where.md and docs/api/reference.md showed db.selectFrom('users').whereRaw('LOWER(email) = ?', ['john@example.com']). That builder's whereRaw takes a single fragment. The type rejects a second argument, but at runtime it was dropped: SQLite returned [] and Postgres raised a syntax error.
*Rawmethods:selectRaw,whereRaw,orWhereRaw,groupByRaw,havingRaw,orderByRawandselectFromSub.selectRawnow throw when given values. Each message only suggests alternatives that work for that clause.- Still allowed:
undefined,[], and point-free collection callbacks such asfragments.forEach(q.whereRaw). raw():raw('x = ?', value), the quickest edit away from the old form, also dropped its value. It now throws, and an overload makes it a type error.- Docs: both pages now use the
rawtagged template ondb.selectFrom.where.mddocuments both spellings for model queries.
Breaking
Postgres: values in a lone array are now bound one by one. The array used to reach Bun.sql whole, so the parameter's type decided how it was sent, and a few calls depended on that:
- json/jsonb. Bun.sql sends an array bound to a json/jsonb parameter as JSON.
whereRaw('tags @> ?', ['bun'])used to compare against["bun"]and now binds the string'bun', which can match other rows, including inupdate()anddelete(). Wrap an array meant as one value:whereRaw('tags @> ?', [['bun']]). Arrays inside the bindings pass through untouched for this reason. Separate arguments such as('US', ['bun'])are unchanged. - Numbers compared with text.
whereRaw("meta->>'id' = ?", [5])now throwsoperator does not exist: text = integer, aswhereRaw(sql, 5)already did. Pass a string or cast. [null]/[undefined]now bind NULL instead of''.
No whereRaw call in stacksjs/stacks passes array bindings.
Select builder: *Raw methods and raw(string) now throw on values they used to drop. The only calls affected are ones that were already producing an unbound ?.
Tests
test/orm.where-raw-bindings.test.ts(SQLite, 22). All six cases from the issue,orWhereRaw,whereGroup, the read/aggregate/paging terminals,update/delete/increment/decrement, empty andundefinedbindings, wrapped arrays, typed arrays, frozen arrays, and mutation after the call. 16 of the first 19 fail againstmain.test/orm.where-raw-bindings.pg.test.ts(live Postgres, subprocess, skipped without a server). Multi-value arrays on reads and writes, plus the jsonb spellings, including the behaviour change above. Againstmainthe multi-value checks fail with 08P01.test/client.raw-bindings-refused.test.ts(12). Refusal on every*Rawmethod and onraw(string), per-clause messages, allowed trailing arguments, and point-free callbacks.src/__tests__/type-usage-compile.ts. Both ORM spellings compile;raw('x = ?', v)and clientwhereRaw(frag, bindings)do not.
Locally: full suite 2218 pass / 0 fail / 10 skip, root and package tsc clean, pickier . at its 330-warning baseline, bun run build emits the new overloads.
🤖 Generated with Claude Code