ReviewOS

also looking at this

stacks/bun-query-builder

fix(orm)!: accept whereRaw bindings as one array

#1148
Merged glennmichael123 wants to merge fix/1146-where-raw-array-bindings into main
10 files +742 -27
9 days ago

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 1 when the array came first, and Binding expected string, … when it did not.
  • Any write. SqliteExecutor.run passes params unspread, so delete(), update() and increment() 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.

  • *Raw methods: selectRaw, whereRaw, orWhereRaw, groupByRaw, havingRaw, orderByRaw and selectFromSub.selectRaw now throw when given values. Each message only suggests alternatives that work for that clause.
  • Still allowed: undefined, [], and point-free collection callbacks such as fragments.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 raw tagged template on db.selectFrom. where.md documents 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 in update() and delete(). 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 throws operator does not exist: text = integer, as whereRaw(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 and undefined bindings, wrapped arrays, typed arrays, frozen arrays, and mutation after the call. 16 of the first 19 fail against main.
  • 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. Against main the multi-value checks fail with 08P01.
  • test/client.raw-bindings-refused.test.ts (12). Refusal on every *Raw method and on raw(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 client whereRaw(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

10 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.