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
packages/bun-query-builder/src/client.tsmodified+42-11
Changes to packages/bun-query-builder/src/client.ts
@@ -8079,23 +8079,54 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial
80798079
80808080 let sql = `UPDATE ${table} SET ${setClauses.join(',')}`
80818081
8082 // Build WHERE clause
8082 // Build WHERE clause.
8083 //
8084 // Every branch below either appends a predicate or throws. `conditions`
8085 // is a required parameter, so there is no shape of it that means "every
8086 // row" — passing one is the caller saying they have a filter. Anything
8087 // this cannot turn into a predicate used to fall straight through to an
8088 // UPDATE with no WHERE, which rewrote the whole table and reported the
8089 // full count as success. See #1112, and #1101 for the same defect in
8090 // updateTable().
80838091 if (Array.isArray(conditions)) {
8084 sql += ` WHERE ${conditions[0]}${conditions[1]}${getPlaceholder(params.length + 1)}`
8092 // Separated by spaces: the three parts used to be concatenated bare,
8093 // which is fine for `=` and `>=` and produces `namelike?` for every
8094 // word operator the allowed set contains.
8095 const safeOperator = assertSafeWhereOperator(String(conditions[1]), 'updateMany')
8096 sql += ` WHERE ${conditions[0]} ${safeOperator} ${getPlaceholder(params.length + 1)}`
80858097 params.push(conditions[2])
80868098 }
8087 else if (conditions && typeof conditions === 'object' && !('raw' in conditions)) {
8099 // A fragment is inside the declared `WhereExpression` type (through
8100 // `WhereRaw`) and `raw` is a public export, so `updateMany(t, raw('id =
8101 // 1'), data)` typechecks and reads like the intended way to express a
8102 // condition this signature cannot otherwise carry. It matched no branch.
8103 else if (isRawExpression(conditions)) {
8104 sql += ` WHERE ${conditions.raw}`
8105 }
8106 else if (isBoundSqlExpression(conditions)) {
8107 const rendered = renderBoundSqlExpression(conditions, params.length + 1)
8108 sql += ` WHERE ${rendered.text}`
8109 params.push(...rendered.parameters)
8110 }
8111 else if (conditions && typeof conditions === 'object' && Object.keys(conditions).length > 0) {
80888112 const condKeys = Object.keys(conditions)
80898113 const condLen = condKeys.length
8090 if (condLen > 0) {
8091 const baseIdx = params.length
8092 const whereClauses: string[] = Array.from({ length: condLen })
8093 for (let i = 0; i < condLen; i++) {
8094 whereClauses[i] = `${condKeys[i]}=${getPlaceholder(baseIdx + i + 1)}`
8095 params.push((conditions as any)[condKeys[i]])
8096 }
8097 sql += ` WHERE ${whereClauses.join(' AND ')}`
8114 const baseIdx = params.length
8115 const whereClauses: string[] = Array.from({ length: condLen })
8116 for (let i = 0; i < condLen; i++) {
8117 whereClauses[i] = `${condKeys[i]}=${getPlaceholder(baseIdx + i + 1)}`
8118 params.push((conditions as any)[condKeys[i]])
80988119 }
8120 sql += ` WHERE ${whereClauses.join(' AND ')}`
8121 }
8122 // The empty object is the one that bites in production: `conditions` is
8123 // usually built from request input, and `updateMany(t, buildFilter(req.query), data)`
8124 // rewrote every row the moment the filter came back empty.
8125 else if (conditions && typeof conditions === 'object') {
8126 throw new TypeError('[query-builder] updateMany(): an empty object is not a filter. Pass a condition, or use updateTable(table).set(data).execute() if updating every row is intended.')
8127 }
8128 else {
8129 throw new TypeError(`[query-builder] updateMany(): expected a condition, got ${conditions === null ? 'null' : typeof conditions}. Refusing to run an UPDATE with no WHERE — use updateTable(table).set(data).execute() if updating every row is intended.`)
80998130 }
81008131
81018132 return _sql.unsafe(sql, params).execute()