Skip to content

Commit e9d0fd8

Browse files
kevin-dpautofix-ci[bot]claude
authored
fix: Support boolean column filters in where() and having() (#1304)
* Unit tests for boolean column filters * ci: apply automated fixes * fix: Support bare boolean column refs in where() and having() clauses Convert ref proxies to PropRef expressions before the isExpressionLike validation, allowing idiomatic boolean filters like `.where(({ u }) => u.active)` and `.having(({ s }) => s.isActive)`. Closes #1303 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: add changeset for boolean column filters fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f13819f commit e9d0fd8

4 files changed

Lines changed: 114 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/db': patch
3+
---
4+
5+
Support bare boolean column references in `where()` and `having()` clauses. Previously, filtering on a boolean column required `eq(col.active, true)`. Now you can write `.where(({ u }) => u.active)` and `.where(({ u }) => not(u.active))` directly.

packages/db/src/query/builder/index.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import {
2121
createRefProxy,
2222
createRefProxyWithSelected,
23+
isRefProxy,
2324
toExpression,
2425
} from './ref-proxy.js'
2526
import type { NamespacedRow, SingleResult } from '../../types.js'
@@ -366,7 +367,14 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
366367
where(callback: WhereCallback<TContext>): QueryBuilder<TContext> {
367368
const aliases = this._getCurrentAliases()
368369
const refProxy = createRefProxy(aliases) as RefsForContext<TContext>
369-
const expression = callback(refProxy)
370+
const rawExpression = callback(refProxy)
371+
372+
// Allow bare boolean column references like `.where(({ u }) => u.active)`
373+
// by converting ref proxies to PropRef expressions, the same way helper
374+
// functions like `not()` and `eq()` do via `toExpression()`.
375+
const expression = isRefProxy(rawExpression)
376+
? toExpression(rawExpression)
377+
: rawExpression
370378

371379
// Validate that the callback returned a valid expression
372380
// This catches common mistakes like using JavaScript comparison operators (===, !==, etc.)
@@ -419,7 +427,14 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
419427
? createRefProxyWithSelected(aliases)
420428
: createRefProxy(aliases)
421429
) as RefsForContext<TContext>
422-
const expression = callback(refProxy)
430+
const rawExpression = callback(refProxy)
431+
432+
// Allow bare boolean column references like `.having(({ $selected }) => $selected.isActive)`
433+
// by converting ref proxies to PropRef expressions, the same way helper
434+
// functions like `not()` and `eq()` do via `toExpression()`.
435+
const expression = isRefProxy(rawExpression)
436+
? toExpression(rawExpression)
437+
: rawExpression
423438

424439
// Validate that the callback returned a valid expression
425440
// This catches common mistakes like using JavaScript comparison operators (===, !==, etc.)

packages/db/tests/query/group-by.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,52 @@ function createGroupByTests(autoIndex: `off` | `eager`): void {
864864
// No customer has total > 1000 (max is 700)
865865
expect(impossibleFilter.size).toBe(0)
866866
})
867+
868+
test(`having with bare boolean selected field`, () => {
869+
// Select a computed boolean into the result, then use it directly in having
870+
const highVolumeCustomers = createLiveQueryCollection({
871+
startSync: true,
872+
query: (q) =>
873+
q
874+
.from({ orders: ordersCollection })
875+
.groupBy(({ orders }) => orders.customer_id)
876+
.select(({ orders }) => ({
877+
customer_id: orders.customer_id,
878+
order_count: count(orders.id),
879+
is_high_volume: gt(count(orders.id), 2),
880+
}))
881+
.having(({ $selected }) => $selected.is_high_volume),
882+
})
883+
884+
// Only customer 1 has more than 2 orders (3 orders)
885+
expect(highVolumeCustomers.size).toBe(1)
886+
expect(highVolumeCustomers.get(1)?.customer_id).toBe(1)
887+
expect(highVolumeCustomers.get(1)?.is_high_volume).toBe(true)
888+
})
889+
890+
test(`having with negated boolean selected field`, () => {
891+
// Using not() with a bare boolean selected field
892+
const lowVolumeCustomers = createLiveQueryCollection({
893+
startSync: true,
894+
query: (q) =>
895+
q
896+
.from({ orders: ordersCollection })
897+
.groupBy(({ orders }) => orders.customer_id)
898+
.select(({ orders }) => ({
899+
customer_id: orders.customer_id,
900+
order_count: count(orders.id),
901+
is_high_volume: gt(count(orders.id), 2),
902+
}))
903+
.having(({ $selected }) => not($selected.is_high_volume)),
904+
})
905+
906+
// Customers 2 and 3 have 2 orders each (not > 2)
907+
expect(lowVolumeCustomers.size).toBe(2)
908+
expect(lowVolumeCustomers.get(2)?.customer_id).toBe(2)
909+
expect(lowVolumeCustomers.get(3)?.customer_id).toBe(3)
910+
expect(lowVolumeCustomers.get(2)?.is_high_volume).toBe(false)
911+
expect(lowVolumeCustomers.get(3)?.is_high_volume).toBe(false)
912+
})
867913
})
868914

869915
describe(`Live Updates with GROUP BY`, () => {

packages/db/tests/query/where.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,52 @@ function createWhereTests(autoIndex: `off` | `eager`): void {
669669

670670
expect(complexQuery.size).toBe(2) // Alice (dept 1, 75k), Eve (dept 2, age 25)
671671
})
672+
673+
test(`bare boolean column reference as where filter`, () => {
674+
// Using a boolean column directly (without eq()) should filter truthy rows
675+
const activeEmployees = createLiveQueryCollection({
676+
startSync: true,
677+
query: (q) =>
678+
q
679+
.from({ emp: employeesCollection })
680+
.where(({ emp }) => emp.active)
681+
.select(({ emp }) => ({
682+
id: emp.id,
683+
name: emp.name,
684+
active: emp.active,
685+
})),
686+
})
687+
688+
expect(activeEmployees.size).toBe(4) // Alice, Bob, Diana, Eve
689+
expect(activeEmployees.toArray.every((emp) => emp.active)).toBe(true)
690+
691+
// Verify the correct employees are included
692+
const ids = activeEmployees.toArray.map((emp) => emp.id).sort()
693+
expect(ids).toEqual([1, 2, 4, 5])
694+
})
695+
696+
test(`negated boolean column reference as where filter`, () => {
697+
// Using not() with a bare boolean column should filter falsy rows
698+
const inactiveEmployees = createLiveQueryCollection({
699+
startSync: true,
700+
query: (q) =>
701+
q
702+
.from({ emp: employeesCollection })
703+
.where(({ emp }) => not(emp.active))
704+
.select(({ emp }) => ({
705+
id: emp.id,
706+
name: emp.name,
707+
active: emp.active,
708+
})),
709+
})
710+
711+
expect(inactiveEmployees.size).toBe(2) // Charlie, Frank
712+
expect(inactiveEmployees.toArray.every((emp) => !emp.active)).toBe(true)
713+
714+
// Verify the correct employees are included
715+
const ids = inactiveEmployees.toArray.map((emp) => emp.id).sort()
716+
expect(ids).toEqual([3, 6])
717+
})
672718
})
673719

674720
describe(`String Operators`, () => {

0 commit comments

Comments
 (0)