also looking at this
fix(client): an array in object-form where() means IN on writes too
#1127
2 files
+189
-21
| @@ -29,6 +29,35 @@ interface BoundSqlExpression { | ||
| 29 | 29 | parameters?: readonly unknown[] |
| 30 | 30 | } |
| 31 | 31 | |
| 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 | */ | |
| 49 | function 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 | ||
| 32 | 61 | function isBoundSqlExpression(expr: unknown): expr is BoundSqlExpression { |
| 33 | 62 | return typeof expr === 'object' |
| 34 | 63 | && expr !== null |
| @@ -7384,13 +7413,7 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial | ||
| 7384 | 7413 | const keys = Object.keys(expr) |
| 7385 | 7414 | if (keys.length === 0) |
| 7386 | 7415 | 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)) | |
| 7394 | 7417 | appendPredicate(conditions.join(' AND ')) |
| 7395 | 7418 | built = _sql.unsafe(sqlText, params) |
| 7396 | 7419 | return this |
| @@ -7688,12 +7711,7 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial | ||
| 7688 | 7711 | const keys = Object.keys(expr) |
| 7689 | 7712 | if (keys.length === 0) |
| 7690 | 7713 | 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)) | |
| 7697 | 7715 | appendPredicate(conditions.join(' AND ')) |
| 7698 | 7716 | built = null |
| 7699 | 7717 | return this |
| @@ -8487,14 +8505,8 @@ export function createQueryBuilder<DB extends AnyDatabaseSchema>(state?: Partial | ||
| 8487 | 8505 | params.push(...rendered.parameters) |
| 8488 | 8506 | } |
| 8489 | 8507 | 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)) | |
| 8498 | 8510 | sql += ` WHERE ${whereClauses.join(' AND ')}` |
| 8499 | 8511 | } |
| 8500 | 8512 | // The empty object is the one that bites in production: `conditions` is |