ReviewOS

also looking at this

stacks/bun-query-builder

fix(client): an array in object-form where() means IN on writes too

#1127
Merged glennmichael123 wants to merge fix/1114-array-in-write-where into main
2 files +189 -21
packages/bun-query-builder/src/client.tsmodified+33-21
Changes to packages/bun-query-builder/src/client.ts
@@ -29,6 +29,35 @@ interface BoundSqlExpression {
2929 parameters?: readonly unknown[]
3030}
3131
32/**
33 * Render one `{ column: value }` entry of an object-form where() as a
34 * predicate, appending its bound parameters to `params`.
35 *
36 * An array value means IN. The select builder has read it that way since
37 * #1013/#1083; the write builders bound the whole array to a single
38 * placeholder, so `where({ id: [3, 2] })` selected two rows on a SELECT and
39 * matched nothing on an UPDATE or a DELETE — silently, reporting success. The
40 * bulk delete of a set of ids is exactly the call people write it for.
41 *
42 * An empty array renders as `1 = 0` rather than disappearing, which is what
43 * the select builder does and the only safe reading on a write: a filter the
44 * caller supplied must never widen to "every row".
45 *
46 * Shared by all three write paths so they cannot drift apart again — the
47 * divergence, not any one branch, is what kept producing these. See #1114.
48 */
49function renderColumnCondition(column: string, value: unknown, params: unknown[]): string {
50 if (Array.isArray(value)) {
51 const placeholders = getPlaceholders(value.length, params.length + 1)
52 const clause = renderInPredicate(column, value, false, placeholders)
53 params.push(...value)
54 return clause
55 }
56 const clause = `${column} = ${getPlaceholder(params.length + 1)}`
57 params.push(value)
58 return clause
59}
60
3261function isBoundSqlExpression(expr: unknown): expr is BoundSqlExpression {
3362 return typeof expr === 'object'
3463 && expr !== null
@@ -7384,13 +7413,7 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial
73847413 const keys = Object.keys(expr)
73857414 if (keys.length === 0)
73867415 throw new TypeError('[query-builder] updateTable.where({}): an empty object is not a filter. Pass a condition, or drop the where() call if updating every row is intended.')
7387 const len = keys.length
7388 const baseIdx = params.length
7389 const conditions: string[] = Array.from({ length: len })
7390 for (let i = 0; i < len; i++) {
7391 conditions[i] = `${quoteId(keys[i])} = ${getPlaceholder(baseIdx + i + 1)}`
7392 params.push((expr as any)[keys[i]])
7393 }
7416 const conditions = keys.map(key => renderColumnCondition(quoteId(key), (expr as any)[key], params))
73947417 appendPredicate(conditions.join(' AND '))
73957418 built = _sql.unsafe(sqlText, params)
73967419 return this
@@ -7688,12 +7711,7 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial
76887711 const keys = Object.keys(expr)
76897712 if (keys.length === 0)
76907713 throw new TypeError('[query-builder] deleteFrom.where({}): an empty object is not a filter. Pass a condition, or drop the where() call if deleting every row is intended.')
7691 const conditions: string[] = []
7692 for (const key of keys) {
7693 const paramIndex = delParams.length + 1
7694 conditions.push(`${quoteId(key)} = ${getPlaceholder(paramIndex)}`)
7695 delParams.push((expr as any)[key])
7696 }
7714 const conditions = keys.map(key => renderColumnCondition(quoteId(key), (expr as any)[key], delParams))
76977715 appendPredicate(conditions.join(' AND '))
76987716 built = null
76997717 return this
@@ -8487,14 +8505,8 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial
84878505 params.push(...rendered.parameters)
84888506 }
84898507 else if (conditions && typeof conditions === 'object' && Object.keys(conditions).length > 0) {
8490 const condKeys = Object.keys(conditions)
8491 const condLen = condKeys.length
8492 const baseIdx = params.length
8493 const whereClauses: string[] = Array.from({ length: condLen })
8494 for (let i = 0; i < condLen; i++) {
8495 whereClauses[i] = `${condKeys[i]}=${getPlaceholder(baseIdx + i + 1)}`
8496 params.push((conditions as any)[condKeys[i]])
8497 }
8508 const whereClauses = Object.keys(conditions).map(key =>
8509 renderColumnCondition(key, (conditions as any)[key], params))
84988510 sql += ` WHERE ${whereClauses.join(' AND ')}`
84998511 }
85008512 // The empty object is the one that bites in production: `conditions` is