From d24d6e865181cba6baa6b36ac769b8fa947a9bb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Jan 2026 22:33:28 +0000 Subject: [PATCH 1/5] fix: support $selected in orderBy when using fn.select The orderBy method was only checking for regular select clause (this.query.select) to determine if $selected should be exposed. When using fn.select, the callback is stored in fnSelect instead, so $selected was not available in orderBy callbacks. This change updates orderBy to check for both select and fnSelect, enabling queries like: ```ts q.from({ user: usersCollection }) .fn.select((row) => ({ name: row.user.name, salary: row.user.salary })) .orderBy(({ $selected }) => $selected.salary) ``` https://claude.ai/code/session_01EPRCG2K8348FMNWzjPiacC --- packages/db/src/query/builder/index.ts | 6 +- .../tests/query/functional-variants.test-d.ts | 52 +++++++ .../tests/query/functional-variants.test.ts | 134 ++++++++++++++++++ 3 files changed, 189 insertions(+), 3 deletions(-) diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index 6ef4d1ddc..75bf36640 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -34,8 +34,8 @@ import type { import type { CompareOptions, Context, - GetResult, FunctionalHavingRow, + GetResult, GroupByCallback, JoinOnCallback, MergeContextForJoinCallback, @@ -516,9 +516,9 @@ export class BaseQueryBuilder { options: OrderByDirection | OrderByOptions = `asc`, ): QueryBuilder { const aliases = this._getCurrentAliases() - // Add $selected namespace if SELECT clause exists + // Add $selected namespace if SELECT clause exists (either regular or functional) const refProxy = ( - this.query.select + this.query.select || this.query.fnSelect ? createRefProxyWithSelected(aliases) : createRefProxy(aliases) ) as RefsForContext diff --git a/packages/db/tests/query/functional-variants.test-d.ts b/packages/db/tests/query/functional-variants.test-d.ts index 61b571c5f..f39083047 100644 --- a/packages/db/tests/query/functional-variants.test-d.ts +++ b/packages/db/tests/query/functional-variants.test-d.ts @@ -468,4 +468,56 @@ describe(`Functional Variants Types`, () => { }> >() }) + + test(`fn.select with orderBy has access to $selected`, () => { + const liveCollection = createLiveQueryCollection({ + query: (q) => + q + .from({ user: usersCollection }) + .fn.select((row) => ({ + name: row.user.name, + salaryInThousands: row.user.salary / 1000, + ageCategory: + row.user.age > 30 + ? (`senior` as const) + : row.user.age > 25 + ? (`mid` as const) + : (`junior` as const), + })) + .orderBy(({ $selected }) => $selected.salaryInThousands), + }) + + const results = liveCollection.toArray + expectTypeOf(results).toEqualTypeOf< + Array<{ + name: string + salaryInThousands: number + ageCategory: `senior` | `mid` | `junior` + }> + >() + }) + + test(`fn.select with multiple orderBy clauses using $selected`, () => { + const liveCollection = createLiveQueryCollection({ + query: (q) => + q + .from({ user: usersCollection }) + .fn.select((row) => ({ + displayName: row.user.name, + isActive: row.user.active, + salary: row.user.salary, + })) + .orderBy(({ $selected }) => $selected.isActive, `desc`) + .orderBy(({ $selected }) => $selected.salary), + }) + + const results = liveCollection.toArray + expectTypeOf(results).toEqualTypeOf< + Array<{ + displayName: string + isActive: boolean + salary: number + }> + >() + }) }) diff --git a/packages/db/tests/query/functional-variants.test.ts b/packages/db/tests/query/functional-variants.test.ts index 62f985654..b807d14ad 100644 --- a/packages/db/tests/query/functional-variants.test.ts +++ b/packages/db/tests/query/functional-variants.test.ts @@ -477,6 +477,140 @@ describe(`Functional Variants Query`, () => { }) }) + describe(`fn.select with orderBy using $selected`, () => { + let usersCollection: ReturnType + + beforeEach(() => { + usersCollection = createUsersCollection() + }) + + test(`should allow orderBy to reference $selected fields from fn.select`, () => { + const liveCollection = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ user: usersCollection }) + .fn.select((row) => ({ + name: row.user.name, + salaryInThousands: row.user.salary / 1000, + })) + .orderBy(({ $selected }) => $selected.salaryInThousands, `desc`), + }) + + const results = liveCollection.toArray + + expect(results).toHaveLength(5) + // Should be ordered by salary descending + expect(results.map((r) => r.name)).toEqual([ + `Charlie`, // 85k + `Alice`, // 75k + `Dave`, // 65k + `Eve`, // 55k + `Bob`, // 45k + ]) + }) + + test(`should allow orderBy with $selected on computed string fields`, () => { + const liveCollection = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ user: usersCollection }) + .fn.select((row) => ({ + displayName: `${row.user.name} (${row.user.age})`, + lastName: row.user.name.toLowerCase(), + })) + .orderBy(({ $selected }) => $selected.lastName), + }) + + const results = liveCollection.toArray + + expect(results).toHaveLength(5) + // Should be ordered alphabetically by lowercase name + expect(results.map((r) => r.displayName)).toEqual([ + `Alice (25)`, + `Bob (19)`, + `Charlie (30)`, + `Dave (22)`, + `Eve (28)`, + ]) + }) + + test(`should allow multiple orderBy clauses with fn.select`, () => { + const liveCollection = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ user: usersCollection }) + .fn.select((row) => ({ + name: row.user.name, + isActive: row.user.active, + salary: row.user.salary, + })) + .orderBy(({ $selected }) => $selected.isActive, `desc`) + .orderBy(({ $selected }) => $selected.salary, `desc`), + }) + + const results = liveCollection.toArray + + expect(results).toHaveLength(5) + // Should be ordered by active (true first), then by salary desc + // Active users: Alice (75k), Dave (65k), Eve (55k), Bob (45k) + // Inactive users: Charlie (85k) + expect(results.map((r) => r.name)).toEqual([ + `Alice`, // active, 75k + `Dave`, // active, 65k + `Eve`, // active, 55k + `Bob`, // active, 45k + `Charlie`, // inactive, 85k + ]) + }) + + test(`should react to changes when using fn.select with orderBy`, () => { + const liveCollection = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ user: usersCollection }) + .fn.select((row) => ({ + name: row.user.name, + salary: row.user.salary, + })) + .orderBy(({ $selected }) => $selected.salary), + }) + + // Initial order (ascending by salary) + expect(liveCollection.toArray.map((r) => r.name)).toEqual([ + `Bob`, // 45k + `Eve`, // 55k + `Dave`, // 65k + `Alice`, // 75k + `Charlie`, // 85k + ]) + + // Update Bob's salary to be the highest + const bob = sampleUsers.find((u) => u.name === `Bob`)! + const richBob = { ...bob, salary: 100000 } + usersCollection.utils.begin() + usersCollection.utils.write({ type: `update`, value: richBob }) + usersCollection.utils.commit() + + // Bob should now be at the end (highest salary) + expect(liveCollection.toArray.map((r) => r.name)).toEqual([ + `Eve`, // 55k + `Dave`, // 65k + `Alice`, // 75k + `Charlie`, // 85k + `Bob`, // 100k + ]) + + // Clean up + usersCollection.utils.begin() + usersCollection.utils.write({ type: `update`, value: bob }) + usersCollection.utils.commit() + }) + }) + describe(`combinations`, () => { let usersCollection: ReturnType let departmentsCollection: ReturnType From 9e8677eb4a1826b52b981299333af23218a3041c Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Mon, 26 Jan 2026 16:21:01 -0700 Subject: [PATCH 2/5] fix: extend $selected support to having method for fn.select consistency Also adds tests for orderBy with table refs after fn.select. Co-Authored-By: Claude Opus 4.5 --- .../fix-fnselect-selected-orderby-having.md | 5 ++ packages/db/src/query/builder/index.ts | 4 +- .../tests/query/functional-variants.test.ts | 55 +++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-fnselect-selected-orderby-having.md diff --git a/.changeset/fix-fnselect-selected-orderby-having.md b/.changeset/fix-fnselect-selected-orderby-having.md new file mode 100644 index 000000000..202feecf5 --- /dev/null +++ b/.changeset/fix-fnselect-selected-orderby-having.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db': patch +--- + +Fix `$selected` namespace availability in `orderBy` and `having` when using `fn.select`. Previously, the `$selected` namespace was only available when using regular `.select()`, not functional `fn.select()`. diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index 75bf36640..dcdfe6c0e 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -413,9 +413,9 @@ export class BaseQueryBuilder { */ having(callback: WhereCallback): QueryBuilder { const aliases = this._getCurrentAliases() - // Add $selected namespace if SELECT clause exists + // Add $selected namespace if SELECT clause exists (either regular or functional) const refProxy = ( - this.query.select + this.query.select || this.query.fnSelect ? createRefProxyWithSelected(aliases) : createRefProxy(aliases) ) as RefsForContext diff --git a/packages/db/tests/query/functional-variants.test.ts b/packages/db/tests/query/functional-variants.test.ts index b807d14ad..aa0714760 100644 --- a/packages/db/tests/query/functional-variants.test.ts +++ b/packages/db/tests/query/functional-variants.test.ts @@ -609,6 +609,61 @@ describe(`Functional Variants Query`, () => { usersCollection.utils.write({ type: `update`, value: bob }) usersCollection.utils.commit() }) + + test(`should allow orderBy with table refs after fn.select`, () => { + const liveCollection = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ user: usersCollection }) + .fn.select((row) => ({ + displayName: row.user.name, + salary: row.user.salary, + })) + .orderBy(({ user }) => user.age), + }) + + const results = liveCollection.toArray + + expect(results).toHaveLength(5) + // Should be ordered by age (from original table, not $selected) + expect(results.map((r) => r.displayName)).toEqual([ + `Bob`, // 19 + `Dave`, // 22 + `Alice`, // 25 + `Eve`, // 28 + `Charlie`, // 30 + ]) + }) + + test(`should allow orderBy with both table refs and $selected`, () => { + const liveCollection = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ user: usersCollection }) + .fn.select((row) => ({ + name: row.user.name, + salaryTier: row.user.salary > 60000 ? `high` : `low`, + })) + .orderBy(({ $selected }) => $selected.salaryTier) + .orderBy(({ user }) => user.age, `desc`), + }) + + const results = liveCollection.toArray + + expect(results).toHaveLength(5) + // First by salaryTier (high < low alphabetically), then by age desc + // High tier (>60k): Charlie (30), Alice (25), Dave (22) + // Low tier (<=60k): Eve (28), Bob (19) + expect(results.map((r) => r.name)).toEqual([ + `Charlie`, // high, 30 + `Alice`, // high, 25 + `Dave`, // high, 22 + `Eve`, // low, 28 + `Bob`, // low, 19 + ]) + }) }) describe(`combinations`, () => { From 7e45f7127ba3b05814209b8c8c9a3d718877e623 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Mon, 26 Jan 2026 16:30:55 -0700 Subject: [PATCH 3/5] fix: revert having change, keep only tested orderBy fix The having method fix was made for consistency but couldn't be verified with a passing test. Keeping only the orderBy fix which has comprehensive test coverage. Co-Authored-By: Claude Opus 4.5 --- .changeset/fix-fnselect-selected-orderby-having.md | 2 +- packages/db/src/query/builder/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/fix-fnselect-selected-orderby-having.md b/.changeset/fix-fnselect-selected-orderby-having.md index 202feecf5..93a86eb8e 100644 --- a/.changeset/fix-fnselect-selected-orderby-having.md +++ b/.changeset/fix-fnselect-selected-orderby-having.md @@ -2,4 +2,4 @@ '@tanstack/db': patch --- -Fix `$selected` namespace availability in `orderBy` and `having` when using `fn.select`. Previously, the `$selected` namespace was only available when using regular `.select()`, not functional `fn.select()`. +Fix `$selected` namespace availability in `orderBy` when using `fn.select`. Previously, the `$selected` namespace was only available when using regular `.select()`, not functional `fn.select()`. diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index dcdfe6c0e..75bf36640 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -413,9 +413,9 @@ export class BaseQueryBuilder { */ having(callback: WhereCallback): QueryBuilder { const aliases = this._getCurrentAliases() - // Add $selected namespace if SELECT clause exists (either regular or functional) + // Add $selected namespace if SELECT clause exists const refProxy = ( - this.query.select || this.query.fnSelect + this.query.select ? createRefProxyWithSelected(aliases) : createRefProxy(aliases) ) as RefsForContext From d03016a78cd93938494b641e21568c87f8a6239d Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 27 Jan 2026 07:20:08 -0700 Subject: [PATCH 4/5] fix: add $selected support for fn.select with having and fn.having - Added fnSelect check to having method for $selected access - Added test for fn.having with fn.select (no groupBy) - Note: fn.select + groupBy combination requires further work Co-Authored-By: Claude Opus 4.5 --- .../fix-fnselect-selected-orderby-having.md | 2 +- packages/db/src/query/builder/index.ts | 4 ++-- .../tests/query/functional-variants.test.ts | 20 +++++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.changeset/fix-fnselect-selected-orderby-having.md b/.changeset/fix-fnselect-selected-orderby-having.md index 93a86eb8e..a02c29ea0 100644 --- a/.changeset/fix-fnselect-selected-orderby-having.md +++ b/.changeset/fix-fnselect-selected-orderby-having.md @@ -2,4 +2,4 @@ '@tanstack/db': patch --- -Fix `$selected` namespace availability in `orderBy` when using `fn.select`. Previously, the `$selected` namespace was only available when using regular `.select()`, not functional `fn.select()`. +Fix `$selected` namespace availability in `orderBy`, `having`, and `fn.having` when using `fn.select`. Previously, the `$selected` namespace was only available when using regular `.select()`, not functional `fn.select()`. diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index 75bf36640..dcdfe6c0e 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -413,9 +413,9 @@ export class BaseQueryBuilder { */ having(callback: WhereCallback): QueryBuilder { const aliases = this._getCurrentAliases() - // Add $selected namespace if SELECT clause exists + // Add $selected namespace if SELECT clause exists (either regular or functional) const refProxy = ( - this.query.select + this.query.select || this.query.fnSelect ? createRefProxyWithSelected(aliases) : createRefProxy(aliases) ) as RefsForContext diff --git a/packages/db/tests/query/functional-variants.test.ts b/packages/db/tests/query/functional-variants.test.ts index aa0714760..703362210 100644 --- a/packages/db/tests/query/functional-variants.test.ts +++ b/packages/db/tests/query/functional-variants.test.ts @@ -636,6 +636,26 @@ describe(`Functional Variants Query`, () => { ]) }) + test(`should allow fn.having to reference $selected fields from fn.select`, () => { + const liveCollection = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ user: usersCollection }) + .fn.select((row) => ({ + name: row.user.name, + salaryTier: row.user.salary > 60000 ? `high` : `low`, + })) + .fn.having(({ $selected }) => $selected.salaryTier === `high`), + }) + + const results = liveCollection.toArray + + // Only users with salary > 60k: Alice (75k), Charlie (85k), Dave (65k) + expect(results).toHaveLength(3) + expect(results.map((r) => r.name).sort()).toEqual([`Alice`, `Charlie`, `Dave`]) + }) + test(`should allow orderBy with both table refs and $selected`, () => { const liveCollection = createLiveQueryCollection({ startSync: true, From 37725b2b5dd08238c1dce51497520abc888ef42c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:21:08 +0000 Subject: [PATCH 5/5] ci: apply automated fixes --- packages/db/tests/query/functional-variants.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/db/tests/query/functional-variants.test.ts b/packages/db/tests/query/functional-variants.test.ts index 703362210..e9913f5d3 100644 --- a/packages/db/tests/query/functional-variants.test.ts +++ b/packages/db/tests/query/functional-variants.test.ts @@ -653,7 +653,11 @@ describe(`Functional Variants Query`, () => { // Only users with salary > 60k: Alice (75k), Charlie (85k), Dave (65k) expect(results).toHaveLength(3) - expect(results.map((r) => r.name).sort()).toEqual([`Alice`, `Charlie`, `Dave`]) + expect(results.map((r) => r.name).sort()).toEqual([ + `Alice`, + `Charlie`, + `Dave`, + ]) }) test(`should allow orderBy with both table refs and $selected`, () => {