ReviewOS

also looking at this

stacks/bun-query-builder

fix(client): updateMany runs with the caller's predicate, or not at all

#1119
Merged glennmichael123 wants to merge fix/1112-update-many-predicate into main
2 files +199 -11
8 days ago

Closes #1112.

updateMany appended a WHERE only for an array, or for an object that both lacked a raw key and had at least one key. Every other shape produced an UPDATE with no predicate — no error, full affected count reported as success:

await db.updateMany('t', raw('id = 1'), { name: 'T' })   // -> all four rows rewritten
await db.updateMany('t', {},            { name: 'T' })   // -> all four rows rewritten

The fragment form reads like correct code. conditions is declared WhereExpression, which includes WhereRaw, and raw is a public export — so it typechecks and passes review. The empty-object form is the one that reaches production, because conditions is usually built from request input and an empty filter is what an empty query string gives you.

conditions is a required parameter, so no value of it can legitimately mean "every row". Fragments are now rendered as predicates; anything that cannot become one is refused, naming the call that does mean every row — updateTable(table).set(data).execute(). Same remedy as #1101.

Two more defects in the same branch

Both found while writing coverage for the above, both in the array form:

The operator was interpolated unvalidated. Every other builder routes it through assertSafeWhereOperator; this one did not. The array form [col, op, value] is exactly the shape a filter DSL builds from request input, so the operator slot is reachable. The naive payload is stopped by accident — -- comments out the placeholder and the driver's arity check fires — but one that keeps the count at one goes straight through:

await db.updateMany('t', ['id', '>0 OR id>', 0], { name: 'PWNED' })
// UPDATE t SET name=$1 WHERE id>0 OR id>$2   -> every row

Now refused with the same message the other builders use.

The three parts were concatenated bare. Fine for = and >=; for every word operator in WhereOperator — like, in, not in, is, is not — it emitted namelike?, a syntax error. Now space-separated.

Verification

Executed against sqlite, asserting on surviving rows rather than SQL text. 8 of the 11 new tests fail without this change. Full suite 2048 pass / 0 fail, typecheck clean, pickier 0 errors.

The survey behind this turned up two more disagreements between WhereExpression and what updateMany does at runtime, both already tracked by #1114: an array value ({ id: [1,2,3] }) is type-legal and means IN on a SELECT, but binds the whole array to one placeholder here; and a fragment as an object value ({ created_at: sql\now()` }`) is type-legal and gets bound as a plain parameter rather than rendered.

🤖 Generated with Claude Code

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

Sign in to comment on this pull request.