From 9cd93393a0b4efd5c172b84606d79d4d16040387 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 26 Feb 2026 10:17:03 +0100 Subject: [PATCH 1/5] Unit tests for boolean column filters --- packages/db/tests/query/group-by.test.ts | 50 ++++++++++++++++++++++++ packages/db/tests/query/where.test.ts | 46 ++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/packages/db/tests/query/group-by.test.ts b/packages/db/tests/query/group-by.test.ts index 3bc8326ba..d7b5e4030 100644 --- a/packages/db/tests/query/group-by.test.ts +++ b/packages/db/tests/query/group-by.test.ts @@ -864,6 +864,56 @@ function createGroupByTests(autoIndex: `off` | `eager`): void { // No customer has total > 1000 (max is 700) expect(impossibleFilter.size).toBe(0) }) + + test(`having with bare boolean selected field`, () => { + // Select a computed boolean into the result, then use it directly in having + const highVolumeCustomers = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ orders: ordersCollection }) + .groupBy(({ orders }) => orders.customer_id) + .select(({ orders }) => ({ + customer_id: orders.customer_id, + order_count: count(orders.id), + is_high_volume: gt(count(orders.id), 2), + })) + .having( + ({ $selected }) => $selected.is_high_volume, + ), + }) + + // Only customer 1 has more than 2 orders (3 orders) + expect(highVolumeCustomers.size).toBe(1) + expect(highVolumeCustomers.get(1)?.customer_id).toBe(1) + expect(highVolumeCustomers.get(1)?.is_high_volume).toBe(true) + }) + + test(`having with negated boolean selected field`, () => { + // Using not() with a bare boolean selected field + const lowVolumeCustomers = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ orders: ordersCollection }) + .groupBy(({ orders }) => orders.customer_id) + .select(({ orders }) => ({ + customer_id: orders.customer_id, + order_count: count(orders.id), + is_high_volume: gt(count(orders.id), 2), + })) + .having( + ({ $selected }) => not($selected.is_high_volume), + ), + }) + + // Customers 2 and 3 have 2 orders each (not > 2) + expect(lowVolumeCustomers.size).toBe(2) + expect(lowVolumeCustomers.get(2)?.customer_id).toBe(2) + expect(lowVolumeCustomers.get(3)?.customer_id).toBe(3) + expect(lowVolumeCustomers.get(2)?.is_high_volume).toBe(false) + expect(lowVolumeCustomers.get(3)?.is_high_volume).toBe(false) + }) }) describe(`Live Updates with GROUP BY`, () => { diff --git a/packages/db/tests/query/where.test.ts b/packages/db/tests/query/where.test.ts index dda75c0b4..abc91b610 100644 --- a/packages/db/tests/query/where.test.ts +++ b/packages/db/tests/query/where.test.ts @@ -669,6 +669,52 @@ function createWhereTests(autoIndex: `off` | `eager`): void { expect(complexQuery.size).toBe(2) // Alice (dept 1, 75k), Eve (dept 2, age 25) }) + + test(`bare boolean column reference as where filter`, () => { + // Using a boolean column directly (without eq()) should filter truthy rows + const activeEmployees = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ emp: employeesCollection }) + .where(({ emp }) => emp.active) + .select(({ emp }) => ({ + id: emp.id, + name: emp.name, + active: emp.active, + })), + }) + + expect(activeEmployees.size).toBe(4) // Alice, Bob, Diana, Eve + expect(activeEmployees.toArray.every((emp) => emp.active)).toBe(true) + + // Verify the correct employees are included + const ids = activeEmployees.toArray.map((emp) => emp.id).sort() + expect(ids).toEqual([1, 2, 4, 5]) + }) + + test(`negated boolean column reference as where filter`, () => { + // Using not() with a bare boolean column should filter falsy rows + const inactiveEmployees = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ emp: employeesCollection }) + .where(({ emp }) => not(emp.active)) + .select(({ emp }) => ({ + id: emp.id, + name: emp.name, + active: emp.active, + })), + }) + + expect(inactiveEmployees.size).toBe(2) // Charlie, Frank + expect(inactiveEmployees.toArray.every((emp) => !emp.active)).toBe(true) + + // Verify the correct employees are included + const ids = inactiveEmployees.toArray.map((emp) => emp.id).sort() + expect(ids).toEqual([3, 6]) + }) }) describe(`String Operators`, () => { From 0a448b63ab490bb575db198f5bca905bc6473921 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:43:20 +0000 Subject: [PATCH 2/5] ci: apply automated fixes --- packages/db/tests/query/group-by.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/db/tests/query/group-by.test.ts b/packages/db/tests/query/group-by.test.ts index d7b5e4030..0875ec141 100644 --- a/packages/db/tests/query/group-by.test.ts +++ b/packages/db/tests/query/group-by.test.ts @@ -878,9 +878,7 @@ function createGroupByTests(autoIndex: `off` | `eager`): void { order_count: count(orders.id), is_high_volume: gt(count(orders.id), 2), })) - .having( - ({ $selected }) => $selected.is_high_volume, - ), + .having(({ $selected }) => $selected.is_high_volume), }) // Only customer 1 has more than 2 orders (3 orders) @@ -902,9 +900,7 @@ function createGroupByTests(autoIndex: `off` | `eager`): void { order_count: count(orders.id), is_high_volume: gt(count(orders.id), 2), })) - .having( - ({ $selected }) => not($selected.is_high_volume), - ), + .having(({ $selected }) => not($selected.is_high_volume)), }) // Customers 2 and 3 have 2 orders each (not > 2) From 3d701b979ee506d8c6988a3dbd9183a8ae83f464 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 26 Feb 2026 10:51:01 +0100 Subject: [PATCH 3/5] 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 --- packages/db/src/query/builder/index.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index c58ebc087..fe15732bc 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -20,6 +20,7 @@ import { import { createRefProxy, createRefProxyWithSelected, + isRefProxy, toExpression, } from './ref-proxy.js' import type { NamespacedRow, SingleResult } from '../../types.js' @@ -366,7 +367,14 @@ export class BaseQueryBuilder { where(callback: WhereCallback): QueryBuilder { const aliases = this._getCurrentAliases() const refProxy = createRefProxy(aliases) as RefsForContext - const expression = callback(refProxy) + const rawExpression = callback(refProxy) + + // Allow bare boolean column references like `.where(({ u }) => u.active)` + // by converting ref proxies to PropRef expressions, the same way helper + // functions like `not()` and `eq()` do via `toExpression()`. + const expression = isRefProxy(rawExpression) + ? toExpression(rawExpression) + : rawExpression // Validate that the callback returned a valid expression // This catches common mistakes like using JavaScript comparison operators (===, !==, etc.) @@ -419,7 +427,14 @@ export class BaseQueryBuilder { ? createRefProxyWithSelected(aliases) : createRefProxy(aliases) ) as RefsForContext - const expression = callback(refProxy) + const rawExpression = callback(refProxy) + + // Allow bare boolean column references like `.having(({ $selected }) => $selected.isActive)` + // by converting ref proxies to PropRef expressions, the same way helper + // functions like `not()` and `eq()` do via `toExpression()`. + const expression = isRefProxy(rawExpression) + ? toExpression(rawExpression) + : rawExpression // Validate that the callback returned a valid expression // This catches common mistakes like using JavaScript comparison operators (===, !==, etc.) From e7adb44157725a8a569d2076463f9d078c7d40ec Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 26 Feb 2026 11:00:31 +0100 Subject: [PATCH 4/5] chore: add changeset for boolean column filters fix Co-Authored-By: Claude Opus 4.6 --- .changeset/boolean-column-filters.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/boolean-column-filters.md diff --git a/.changeset/boolean-column-filters.md b/.changeset/boolean-column-filters.md new file mode 100644 index 000000000..ab976b0f3 --- /dev/null +++ b/.changeset/boolean-column-filters.md @@ -0,0 +1,5 @@ +--- +"@tanstack/db": patch +--- + +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. From d8ea07270ae6c380800757bd3d63d2de92db4687 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:01:28 +0000 Subject: [PATCH 5/5] ci: apply automated fixes --- .changeset/boolean-column-filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/boolean-column-filters.md b/.changeset/boolean-column-filters.md index ab976b0f3..0b0e685d9 100644 --- a/.changeset/boolean-column-filters.md +++ b/.changeset/boolean-column-filters.md @@ -1,5 +1,5 @@ --- -"@tanstack/db": patch +'@tanstack/db': patch --- 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.