ReviewOS

stacks/bun-query-builder

where(col, 'in', vals) generates 'col in ?' instead of 'col IN (?, ?, ?)' and crashes SQLite

#1013
Closed glennmichael123 opened this 22 days ago · 0 comments
22 days ago

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.

Sign in to comment on this issue.