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 rewrittenThe 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 rowNow 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.
Related, not fixed here
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