ReviewOS

stacks/bun-query-builder

whereJsonContains returns zero rows on Postgres — the bound param is double-encoded

#1091
Closed glennmichael123 opened this 22 days ago · 0 comments
22 days ago

Found on main @ 68fd61c, verified against a live Postgres (not inferred from reading).

whereJsonContains is wrong on Postgres for all inputs, not just edge cases. It returns an empty result set for containment that genuinely holds, silently.

Repro

CREATE TABLE jc (id int primary key, tags jsonb);
INSERT INTO jc VALUES (1, '["bun","sql"]'), (2, '["node"]');
const rows = await db.selectFrom('jc').selectAll()
  .whereJsonContains('tags', ['bun'])
  .execute()
// []           <- row 1 should match
-- the same containment, written literally
SELECT id FROM jc WHERE tags @> '["bun"]';   -- [1]

Emitted SQL is fine; the parameter is not:

SELECT * FROM jc WHERE tags @> $1
params: ["[\"bun\"]"]

Cause

whereJsonContains does whereParams.push(JSON.stringify(json)). Bun's SQL driver already JSON-encodes a JS value bound to a jsonb parameter, so the JSON.stringify makes it happen twice. Postgres receives the JSON string "[\"bun\"]" where an array was intended:

await sql.unsafe('SELECT ($1::jsonb)::text v', [JSON.stringify(['bun'])])
// v: "\"[\\\"bun\\\"]\""      <- a jsonb string, not a jsonb array

A jsonb string never @>-contains anything, so the predicate is vacuously false and the query returns nothing.

Binding the value directly is correct:

await sql.unsafe('SELECT id FROM jc2 WHERE tags @> $1', [JSON.stringify(['bun'])])  // []
await sql.unsafe('SELECT id FROM jc2 WHERE tags @> $1', [['bun']])                  // [1]

Fix

Drop the JSON.stringify on the Postgres branch and push json as-is. The MySQL branch (JSON_CONTAINS) takes a text argument and likely still wants the stringified form — worth checking separately rather than changing both together. The SQLite branch uses json_each membership and binds scalars, so it is unaffected.

Note

This interacts with a guard that has been discussed but not shipped: rejecting an empty containment document (whereJsonContains(col, [])). x @> '[]' is vacuously true of every row, so that guard widens the query — but only once the encoding is fixed and the predicate starts doing anything at all. Fix the encoding first.

Sign in to comment on this issue.