ReviewOS

stacks/bun-query-builder

DELETE builder's where() always emits 'WHERE', producing 'WHERE ... WHERE ...' on chained calls

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

Summary

The DELETE query builder's .where() method hardcodes WHERE on every call. Chaining two .where()s produces invalid SQL with two WHERE clauses instead of WHERE ... AND .... SQLite rejects the prepared statement with near "WHERE": syntax error.

The UPDATE builder gets this right via a getWhereKeyword() helper that flips to AND after the first clause. DELETE just needs the same helper.

Third bug in the where/where-related family alongside #1012 and #1013.

Reproducer

import { db } from '@stacksjs/database' // proxies bun-query-builder

// Crashes
await db.deleteFrom('judge_reviews_likes')
  .where('judge_review_id', '=', 12)
  .where('user_id', '=', 5)
  .execute()

// Works (object form takes a single where call)
await db.deleteFrom('judge_reviews_likes')
  .where({ judge_review_id: 12, user_id: 5 })
  .execute()

// Works (UPDATE has the same surface, but its where() correctly switches to AND)
await db.updateTable('judge_reviews_likes')
  .set({ foo: 'bar' })
  .where('judge_review_id', '=', 12)
  .where('user_id', '=', 5)
  .execute()

Error

SQLiteError: near "WHERE": syntax error
    at prepare (bun:sqlite:345:37)
    at run (bun-query-builder/dist/src/index.js:11772:33)
    at execute (bun-query-builder/dist/src/index.js:11921:39)
    at execute (bun-query-builder/dist/src/index.js:15205:44)

Compiled SQL the crashing query emits:

DELETE FROM "judge_reviews_likes" WHERE "judge_review_id" = ? WHERE "user_id" = ?

Root cause

client.ts:5577-5611 — the DELETE builder's .where():

where(expr: any, op?: string, value?: any) {
  whereCondition = expr
  if (typeof expr === 'string' && op !== undefined) {
    const paramIndex = delParams.length + 1
    sqlText += ` WHERE ${quoteId(expr)} ${op} ${getPlaceholder(paramIndex)}`  // ← always ` WHERE `
    delParams.push(value)
    built = null
    return this
  }
  // ...same hardcoded ` WHERE ` in the array-form and object-form branches
}

Compare to the UPDATE builder at client.ts:5454, which defines:

const getWhereKeyword = () => sqlText.toUpperCase().includes(' WHERE ') ? 'AND' : 'WHERE'

…and uses it for every clause. DELETE is missing this helper.

Suggested fix

Add the same helper at the top of the DELETE builder closure and use it across all three where-shape branches:

const getWhereKeyword = () => sqlText.toUpperCase().includes(' WHERE ') ? 'AND' : 'WHERE'

// in each branch:
sqlText += ` ${getWhereKeyword()} ${quoteId(expr)} ${op} ${getPlaceholder(paramIndex)}`

That mirrors the UPDATE implementation byte-for-byte; DELETE inherits the same semantics for free.

Workaround in the meantime

Use the object form (.where({ a: 1, b: 2 })) which collapses into a single SQL WHERE clause with AND-joined conditions. Caught while implementing a like/unlike toggle — JudgeReview._likeable.unlike() (the framework's own likeable trait, storage/framework/core/orm/src/traits/likeable.ts) uses .where('judge_review_id', '=', id).where('user_id', '=', userId) and crashed on the second click.

Environment

  • bun-query-builder vendored via stacks pantry
  • Bun 1.3.x, SQLite driver

Sign in to comment on this issue.