Found on main @ 68fd61c. Established by reading buildQueryParams — these are HTTP query-param builders, so confirming end-to-end needs a server, but the code is unambiguous on both counts.
BrowserModelQueryBuilder.buildQueryParams() (packages/bun-query-builder/src/browser.ts, ~970) turns the recorded _wheres into a URLSearchParams. Two filters change meaning in transit.
1. orWhere is indistinguishable from where
orWhere records the disjunction correctly:
this._wheres.push({ column, operator: '=', value: operatorOrValue, boolean: 'or' })but where.boolean is never read — buildQueryParams iterates every recorded where and appends each as a parameter, which any receiving API will read as a conjunction:
Model.query().where('role', 'admin').orWhere('role', 'moderator')
// role=admin&role=moderator -> AND, not ORSo the disjunction is silently narrowed to a conjunction. boolean is recorded on every push in this file and read nowhere in it.
2. not in sends the same request as in
else if ((where.operator === 'in' || where.operator === 'not in') && Array.isArray(where.value)) {
params.append(`${where.column}[]`, where.value.join(','))
}Both branches produce column[]=a,b. whereNotIn('status', ['banned']) transmits as whereIn('status', ['banned']) — an inverted filter, which is the worst direction for something usually used to exclude.
Note the neighbouring is / is not cases are distinguished (filter[col][is] vs filter[col][is_not]), so the encoding scheme has room for the distinction; not in just wasn't given one.
Impact
Both are silent. Neither throws, neither logs, and the returned rows look plausible — a superset for the first, a complement for the second.
Not addressed here
What the receiving API is expected to accept. If there is a documented contract for the query-param format, not in needs a spelling in it (e.g. filter[col][not_in]) and boolean needs one too, or orWhere should throw on this builder rather than pretend.