Skip to content

Commit 7099459

Browse files
KyleAMathewsclaudeautofix-ci[bot]
authored
Fix: Enable $selected access in orderBy after fn.select (#1183)
* 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 * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * ci: apply automated fixes --------- Co-authored-by: Claude <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 908d6db commit 7099459

4 files changed

Lines changed: 275 additions & 5 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+
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()`.

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ import type {
3434
import type {
3535
CompareOptions,
3636
Context,
37-
GetResult,
3837
FunctionalHavingRow,
38+
GetResult,
3939
GroupByCallback,
4040
JoinOnCallback,
4141
MergeContextForJoinCallback,
@@ -413,9 +413,9 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
413413
*/
414414
having(callback: WhereCallback<TContext>): QueryBuilder<TContext> {
415415
const aliases = this._getCurrentAliases()
416-
// Add $selected namespace if SELECT clause exists
416+
// Add $selected namespace if SELECT clause exists (either regular or functional)
417417
const refProxy = (
418-
this.query.select
418+
this.query.select || this.query.fnSelect
419419
? createRefProxyWithSelected(aliases)
420420
: createRefProxy(aliases)
421421
) as RefsForContext<TContext>
@@ -516,9 +516,9 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
516516
options: OrderByDirection | OrderByOptions = `asc`,
517517
): QueryBuilder<TContext> {
518518
const aliases = this._getCurrentAliases()
519-
// Add $selected namespace if SELECT clause exists
519+
// Add $selected namespace if SELECT clause exists (either regular or functional)
520520
const refProxy = (
521-
this.query.select
521+
this.query.select || this.query.fnSelect
522522
? createRefProxyWithSelected(aliases)
523523
: createRefProxy(aliases)
524524
) as RefsForContext<TContext>

packages/db/tests/query/functional-variants.test-d.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,4 +468,56 @@ describe(`Functional Variants Types`, () => {
468468
}>
469469
>()
470470
})
471+
472+
test(`fn.select with orderBy has access to $selected`, () => {
473+
const liveCollection = createLiveQueryCollection({
474+
query: (q) =>
475+
q
476+
.from({ user: usersCollection })
477+
.fn.select((row) => ({
478+
name: row.user.name,
479+
salaryInThousands: row.user.salary / 1000,
480+
ageCategory:
481+
row.user.age > 30
482+
? (`senior` as const)
483+
: row.user.age > 25
484+
? (`mid` as const)
485+
: (`junior` as const),
486+
}))
487+
.orderBy(({ $selected }) => $selected.salaryInThousands),
488+
})
489+
490+
const results = liveCollection.toArray
491+
expectTypeOf(results).toEqualTypeOf<
492+
Array<{
493+
name: string
494+
salaryInThousands: number
495+
ageCategory: `senior` | `mid` | `junior`
496+
}>
497+
>()
498+
})
499+
500+
test(`fn.select with multiple orderBy clauses using $selected`, () => {
501+
const liveCollection = createLiveQueryCollection({
502+
query: (q) =>
503+
q
504+
.from({ user: usersCollection })
505+
.fn.select((row) => ({
506+
displayName: row.user.name,
507+
isActive: row.user.active,
508+
salary: row.user.salary,
509+
}))
510+
.orderBy(({ $selected }) => $selected.isActive, `desc`)
511+
.orderBy(({ $selected }) => $selected.salary),
512+
})
513+
514+
const results = liveCollection.toArray
515+
expectTypeOf(results).toEqualTypeOf<
516+
Array<{
517+
displayName: string
518+
isActive: boolean
519+
salary: number
520+
}>
521+
>()
522+
})
471523
})

packages/db/tests/query/functional-variants.test.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,219 @@ describe(`Functional Variants Query`, () => {
477477
})
478478
})
479479

480+
describe(`fn.select with orderBy using $selected`, () => {
481+
let usersCollection: ReturnType<typeof createUsersCollection>
482+
483+
beforeEach(() => {
484+
usersCollection = createUsersCollection()
485+
})
486+
487+
test(`should allow orderBy to reference $selected fields from fn.select`, () => {
488+
const liveCollection = createLiveQueryCollection({
489+
startSync: true,
490+
query: (q) =>
491+
q
492+
.from({ user: usersCollection })
493+
.fn.select((row) => ({
494+
name: row.user.name,
495+
salaryInThousands: row.user.salary / 1000,
496+
}))
497+
.orderBy(({ $selected }) => $selected.salaryInThousands, `desc`),
498+
})
499+
500+
const results = liveCollection.toArray
501+
502+
expect(results).toHaveLength(5)
503+
// Should be ordered by salary descending
504+
expect(results.map((r) => r.name)).toEqual([
505+
`Charlie`, // 85k
506+
`Alice`, // 75k
507+
`Dave`, // 65k
508+
`Eve`, // 55k
509+
`Bob`, // 45k
510+
])
511+
})
512+
513+
test(`should allow orderBy with $selected on computed string fields`, () => {
514+
const liveCollection = createLiveQueryCollection({
515+
startSync: true,
516+
query: (q) =>
517+
q
518+
.from({ user: usersCollection })
519+
.fn.select((row) => ({
520+
displayName: `${row.user.name} (${row.user.age})`,
521+
lastName: row.user.name.toLowerCase(),
522+
}))
523+
.orderBy(({ $selected }) => $selected.lastName),
524+
})
525+
526+
const results = liveCollection.toArray
527+
528+
expect(results).toHaveLength(5)
529+
// Should be ordered alphabetically by lowercase name
530+
expect(results.map((r) => r.displayName)).toEqual([
531+
`Alice (25)`,
532+
`Bob (19)`,
533+
`Charlie (30)`,
534+
`Dave (22)`,
535+
`Eve (28)`,
536+
])
537+
})
538+
539+
test(`should allow multiple orderBy clauses with fn.select`, () => {
540+
const liveCollection = createLiveQueryCollection({
541+
startSync: true,
542+
query: (q) =>
543+
q
544+
.from({ user: usersCollection })
545+
.fn.select((row) => ({
546+
name: row.user.name,
547+
isActive: row.user.active,
548+
salary: row.user.salary,
549+
}))
550+
.orderBy(({ $selected }) => $selected.isActive, `desc`)
551+
.orderBy(({ $selected }) => $selected.salary, `desc`),
552+
})
553+
554+
const results = liveCollection.toArray
555+
556+
expect(results).toHaveLength(5)
557+
// Should be ordered by active (true first), then by salary desc
558+
// Active users: Alice (75k), Dave (65k), Eve (55k), Bob (45k)
559+
// Inactive users: Charlie (85k)
560+
expect(results.map((r) => r.name)).toEqual([
561+
`Alice`, // active, 75k
562+
`Dave`, // active, 65k
563+
`Eve`, // active, 55k
564+
`Bob`, // active, 45k
565+
`Charlie`, // inactive, 85k
566+
])
567+
})
568+
569+
test(`should react to changes when using fn.select with orderBy`, () => {
570+
const liveCollection = createLiveQueryCollection({
571+
startSync: true,
572+
query: (q) =>
573+
q
574+
.from({ user: usersCollection })
575+
.fn.select((row) => ({
576+
name: row.user.name,
577+
salary: row.user.salary,
578+
}))
579+
.orderBy(({ $selected }) => $selected.salary),
580+
})
581+
582+
// Initial order (ascending by salary)
583+
expect(liveCollection.toArray.map((r) => r.name)).toEqual([
584+
`Bob`, // 45k
585+
`Eve`, // 55k
586+
`Dave`, // 65k
587+
`Alice`, // 75k
588+
`Charlie`, // 85k
589+
])
590+
591+
// Update Bob's salary to be the highest
592+
const bob = sampleUsers.find((u) => u.name === `Bob`)!
593+
const richBob = { ...bob, salary: 100000 }
594+
usersCollection.utils.begin()
595+
usersCollection.utils.write({ type: `update`, value: richBob })
596+
usersCollection.utils.commit()
597+
598+
// Bob should now be at the end (highest salary)
599+
expect(liveCollection.toArray.map((r) => r.name)).toEqual([
600+
`Eve`, // 55k
601+
`Dave`, // 65k
602+
`Alice`, // 75k
603+
`Charlie`, // 85k
604+
`Bob`, // 100k
605+
])
606+
607+
// Clean up
608+
usersCollection.utils.begin()
609+
usersCollection.utils.write({ type: `update`, value: bob })
610+
usersCollection.utils.commit()
611+
})
612+
613+
test(`should allow orderBy with table refs after fn.select`, () => {
614+
const liveCollection = createLiveQueryCollection({
615+
startSync: true,
616+
query: (q) =>
617+
q
618+
.from({ user: usersCollection })
619+
.fn.select((row) => ({
620+
displayName: row.user.name,
621+
salary: row.user.salary,
622+
}))
623+
.orderBy(({ user }) => user.age),
624+
})
625+
626+
const results = liveCollection.toArray
627+
628+
expect(results).toHaveLength(5)
629+
// Should be ordered by age (from original table, not $selected)
630+
expect(results.map((r) => r.displayName)).toEqual([
631+
`Bob`, // 19
632+
`Dave`, // 22
633+
`Alice`, // 25
634+
`Eve`, // 28
635+
`Charlie`, // 30
636+
])
637+
})
638+
639+
test(`should allow fn.having to reference $selected fields from fn.select`, () => {
640+
const liveCollection = createLiveQueryCollection({
641+
startSync: true,
642+
query: (q) =>
643+
q
644+
.from({ user: usersCollection })
645+
.fn.select((row) => ({
646+
name: row.user.name,
647+
salaryTier: row.user.salary > 60000 ? `high` : `low`,
648+
}))
649+
.fn.having(({ $selected }) => $selected.salaryTier === `high`),
650+
})
651+
652+
const results = liveCollection.toArray
653+
654+
// Only users with salary > 60k: Alice (75k), Charlie (85k), Dave (65k)
655+
expect(results).toHaveLength(3)
656+
expect(results.map((r) => r.name).sort()).toEqual([
657+
`Alice`,
658+
`Charlie`,
659+
`Dave`,
660+
])
661+
})
662+
663+
test(`should allow orderBy with both table refs and $selected`, () => {
664+
const liveCollection = createLiveQueryCollection({
665+
startSync: true,
666+
query: (q) =>
667+
q
668+
.from({ user: usersCollection })
669+
.fn.select((row) => ({
670+
name: row.user.name,
671+
salaryTier: row.user.salary > 60000 ? `high` : `low`,
672+
}))
673+
.orderBy(({ $selected }) => $selected.salaryTier)
674+
.orderBy(({ user }) => user.age, `desc`),
675+
})
676+
677+
const results = liveCollection.toArray
678+
679+
expect(results).toHaveLength(5)
680+
// First by salaryTier (high < low alphabetically), then by age desc
681+
// High tier (>60k): Charlie (30), Alice (25), Dave (22)
682+
// Low tier (<=60k): Eve (28), Bob (19)
683+
expect(results.map((r) => r.name)).toEqual([
684+
`Charlie`, // high, 30
685+
`Alice`, // high, 25
686+
`Dave`, // high, 22
687+
`Eve`, // low, 28
688+
`Bob`, // low, 19
689+
])
690+
})
691+
})
692+
480693
describe(`combinations`, () => {
481694
let usersCollection: ReturnType<typeof createUsersCollection>
482695
let departmentsCollection: ReturnType<typeof createDepartmentsCollection>

0 commit comments

Comments
 (0)