ReviewOS

stacks/bun-query-builder

A WHERE inside a set() fragment makes the caller's predicate render as AND, folding it into the SET clause

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

Found on main @ a258137, verified by execution against live Postgres 17.

The update builder decides between WHERE and AND by scanning the whole statement text (client.ts ~:6902):

const getWhereKeyword = () => SQL_PATTERNS.WHERE.test(sqlText) ? 'AND' : 'WHERE'

A subquery in set() that contains its own WHERE satisfies that test, so the caller's first predicate is emitted as AND and becomes part of the SET expression.

Repro

await db.updateTable('t')
  .set({ flag: raw('(SELECT count(*) FROM t x WHERE x.id > 3)') })
  .where({ id: 1 })
  .execute()

Emitted:

UPDATE "t" SET "flag" = (SELECT count(*) FROM t x WHERE x.id > 3) AND "id" = $1
--                                                               ^^^ should be WHERE

On Postgres with an integer column this is loud:

argument of AND must be type boolean, not type bigint

Why it is worth fixing rather than leaving to the error

It is loud only because the types happen not to line up. SET "flag" = (<boolean expr>) AND "id" = $1 parses fine when the SET target is boolean — the predicate then silently becomes part of the value being written, and the UPDATE still has no WHERE, so it hits every row.

The same scan is used by whereNull/whereNotNull on this builder, and the delete builder has its own copy.

Suggested fix

Track whether this builder has appended a predicate, rather than inferring it from the statement text. A boolean on the builder cannot be fooled by a subquery, a string literal containing the word, or a column named where.

Found by an automated sweep; the emitted SQL above is from my own reproduction.

Sign in to comment on this issue.