Summary
The three-arg string form .where('col', 'in', [1, 2, 3]) doesn't special-case the 'in' / 'not in' operators. It generates col in ? (a single placeholder), which the underlying driver rejects with SQLiteError: near "?": syntax error. The array form .where(['col', 'in', vals]) works correctly because a separate code path handles it.
This is a sibling bug to #1012 — same API surface (.where()), same root cause (one entry-point shape gets parity treatment, another doesn't).
Reproducer
import { db } from '@stacksjs/database' // proxies bun-query-builder
// Crashes
await db.selectFrom('judge_reviews_likes')
.select(['judge_review_id'])
.where('user_id', '=', 1)
.where('judge_review_id', 'in', [10, 11, 12])
.execute()
// Works (array-form where)
await db.selectFrom('judge_reviews_likes')
.select(['judge_review_id'])
.where(['user_id', '=', 1])
.where(['judge_review_id', 'in', [10, 11, 12]])
.execute()
// Also works (dedicated whereIn)
await db.selectFrom('judge_reviews_likes')
.select(['judge_review_id'])
.where('user_id', '=', 1)
.whereIn('judge_review_id', [10, 11, 12])
.execute()Error
SQLiteError: near "?": syntax error
at prepare (bun:sqlite:345:37)
at query (bun-query-builder/dist/src/index.js:11768:33)
at execute (bun-query-builder/dist/src/index.js:11918:41)Compiled SQL the crashing query emits:
SELECT judge_review_id FROM judge_reviews_likes WHERE user_id = ? AND judge_review_id in ?Root cause
client.ts:3576-3613. The three-arg string entry point (where(expr, op, value) where expr is a string and op is defined) takes this branch:
if (typeof expr === 'string' && op !== undefined) {
const paramIndex = whereParams.length + 1
whereConditions.push(`${String(expr)} ${String(op)} ${getPlaceholder(paramIndex)}`)
whereParams.push(value)
text = `${text} ${getWhereKeyword()} ${String(expr)} ${String(op)} ${getPlaceholder(paramIndex)}`
built = null
return this
}Unconditional single-placeholder emission. The array-format branch a few lines below (3591-3613) DOES check for 'in' / 'not in' and expands to (?, ?, ?) with one placeholder per element, plus pushes each element to whereParams. The two branches should behave identically for the same logical condition.
Suggested fix
Lift the 'in' / 'not in' handling out of the array-form branch (or duplicate the small block into the string-form branch) so both shapes agree:
if (typeof expr === 'string' && op !== undefined) {
const operator = String(op).toLowerCase()
if (operator === 'in' || operator === 'not in') {
const values = Array.isArray(value) ? value : [value]
const placeholders = getPlaceholders(values.length, whereParams.length + 1)
whereConditions.push(`${String(expr)} ${operator.toUpperCase()} (${placeholders})`)
whereParams.push(...values)
text = `${text} ${getWhereKeyword()} ${String(expr)} ${operator.toUpperCase()} (${placeholders})`
built = null
return this
}
const paramIndex = whereParams.length + 1
whereConditions.push(`${String(expr)} ${String(op)} ${getPlaceholder(paramIndex)}`)
whereParams.push(value)
text = `${text} ${getWhereKeyword()} ${String(expr)} ${String(op)} ${getPlaceholder(paramIndex)}`
built = null
return this
}Workaround in the meantime
Use .whereIn('col', values) or the array-form .where(['col', 'in', values]) — both routes hit working code paths.
Environment
- bun-query-builder vendored via stacks pantry
- Bun 1.3.x, SQLite driver
Caught while implementing a "people find this helpful" feature on a Stacks app — same context as #1012. After #1012 unblocked .select('col'), the next call in the chain (.where('judge_review_id', 'in', ids)) hit this.