Skip to content

Commit bafeaa1

Browse files
authored
fix distinct when used with joins (#510)
1 parent 1814f8c commit bafeaa1

File tree

3 files changed

+178
-0
lines changed

3 files changed

+178
-0
lines changed

.changeset/long-ducks-arrive.md

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 a bug where distinct was not applied to queries using a join

packages/db/src/query/optimizer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,7 @@ function applyOptimizations(
660660
orderBy: query.orderBy ? [...query.orderBy] : undefined,
661661
limit: query.limit,
662662
offset: query.offset,
663+
distinct: query.distinct,
663664
fnSelect: query.fnSelect,
664665
fnWhere: query.fnWhere ? [...query.fnWhere] : undefined,
665666
fnHaving: query.fnHaving ? [...query.fnHaving] : undefined,

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

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { concat, createLiveQueryCollection } from "../../src/query/index.js"
33
import { createCollection } from "../../src/collection.js"
44
import { mockSyncCollectionOptions } from "../utils.js"
55
import { DistinctRequiresSelectError } from "../../src/errors"
6+
import { count, eq, gte, not } from "../../src/query/builder/functions.js"
67

78
// Sample data types for comprehensive DISTINCT testing
89
type User = {
@@ -546,6 +547,177 @@ function createDistinctTests(autoIndex: `off` | `eager`): void {
546547
expect(locations).toContain(`Junior`)
547548
})
548549
})
550+
551+
describe(`Distinct with Other Operators`, () => {
552+
let usersCollection: ReturnType<typeof createUsersCollection>
553+
554+
beforeEach(() => {
555+
usersCollection = createUsersCollection(autoIndex)
556+
})
557+
558+
test(`distinct with groupBy - should work with aggregates`, () => {
559+
const distinctGroupedData = createLiveQueryCollection({
560+
startSync: true,
561+
query: (q) =>
562+
q
563+
.from({ users: usersCollection })
564+
.groupBy(({ users }) => users.department)
565+
.select(({ users }) => ({
566+
department: users.department,
567+
user_count: count(users.id),
568+
}))
569+
.distinct(),
570+
})
571+
572+
// Should have 3 distinct department groups: Engineering, Marketing, Sales
573+
expect(distinctGroupedData.size).toBe(3)
574+
575+
const departments = Array.from(distinctGroupedData.values())
576+
const departmentNames = departments.map((d) => d.department).sort()
577+
const userCounts = departments.map((d) => d.user_count).sort()
578+
expect(departmentNames).toEqual([`Engineering`, `Marketing`, `Sales`])
579+
expect(userCounts).toEqual([1, 2, 5])
580+
581+
// Check that counts are correct
582+
const engineeringGroup = departments.find(
583+
(d) => d.department === `Engineering`
584+
)
585+
expect(engineeringGroup?.user_count).toBe(5) // John, Jane, Alice, Diana, Frank
586+
})
587+
588+
test(`distinct with filter - should apply distinct after filtering`, () => {
589+
const distinctFilteredUsers = createLiveQueryCollection({
590+
startSync: true,
591+
query: (q) =>
592+
q
593+
.from({ users: usersCollection })
594+
.where(({ users }) => not(eq(users.country, `USA`)))
595+
.select(({ users }) => ({ country: users.country }))
596+
.distinct(),
597+
})
598+
599+
expect(distinctFilteredUsers.size).toBe(2)
600+
601+
const countries = Array.from(distinctFilteredUsers.values()).map(
602+
(u) => u.country
603+
)
604+
expect(countries).toContain(`Canada`)
605+
expect(countries).toContain(`UK`)
606+
})
607+
608+
test(`distinct with orderBy - should maintain distinct results in order`, () => {
609+
const distinctOrderedCountries = createLiveQueryCollection({
610+
startSync: true,
611+
query: (q) =>
612+
q
613+
.from({ users: usersCollection })
614+
.orderBy(({ users }) => users.country, `asc`)
615+
.select(({ users }) => ({ country: users.country }))
616+
.distinct(),
617+
})
618+
619+
expect(distinctOrderedCountries.size).toBe(3)
620+
621+
const orderedCountries = distinctOrderedCountries.toArray.map(
622+
(u) => u.country
623+
)
624+
expect(orderedCountries).toEqual([`Canada`, `UK`, `USA`])
625+
})
626+
627+
test(`distinct with multiple chained operators`, () => {
628+
const complexQuery = createLiveQueryCollection({
629+
startSync: true,
630+
query: (q) =>
631+
q
632+
.from({ users: usersCollection })
633+
.where(({ users }) => gte(users.salary, 75000))
634+
.orderBy(({ users }) => users.department, `asc`)
635+
.select(({ users }) => ({
636+
department: users.department,
637+
role: users.role,
638+
}))
639+
.distinct(),
640+
})
641+
642+
// Should have distinct department-role combinations for users with salary >= 75000
643+
const results = complexQuery.toArray
644+
expect(results.length).toBeGreaterThan(0)
645+
646+
// Check that we have distinct combinations
647+
const combinations = results.map((r) => `${r.department}-${r.role}`)
648+
const uniqueCombinations = [...new Set(combinations)]
649+
expect(combinations.length).toBe(uniqueCombinations.length)
650+
})
651+
652+
test(`groupBy with distinct on aggregated results`, () => {
653+
const groupedDistinctSalaries = createLiveQueryCollection({
654+
startSync: true,
655+
query: (q) =>
656+
q
657+
.from({ users: usersCollection })
658+
.groupBy(({ users }) => users.city)
659+
.select(({ users }) => ({
660+
count: count(users.id),
661+
}))
662+
.distinct(),
663+
})
664+
665+
// There are 3 distinct counts of users per city
666+
expect(groupedDistinctSalaries.size).toBe(3)
667+
668+
const counts = Array.from(groupedDistinctSalaries.values()).map(
669+
(result) => result.count
670+
)
671+
672+
// All average salaries should be unique (distinct)
673+
const uniqueCounts = [...new Set(counts)]
674+
expect(counts.length).toBe(uniqueCounts.length)
675+
})
676+
677+
test(`distinct with join operations`, () => {
678+
// Create a simple departments collection to join with
679+
const departmentsData = [
680+
{ id: `Engineering`, budget: 1000000 },
681+
{ id: `Marketing`, budget: 500000 },
682+
{ id: `Sales`, budget: 750000 },
683+
]
684+
685+
const departmentsCollection = createCollection(
686+
mockSyncCollectionOptions({
687+
id: `test-departments`,
688+
getKey: (dept) => dept.id,
689+
initialData: departmentsData,
690+
autoIndex,
691+
})
692+
)
693+
694+
const distinctJoinedData = createLiveQueryCollection({
695+
startSync: true,
696+
query: (q) =>
697+
q
698+
.from({ users: usersCollection })
699+
.join(
700+
{ departments: departmentsCollection },
701+
({ users, departments }) => eq(users.department, departments.id)
702+
)
703+
.where(({ users }) => eq(users.active, true))
704+
.select(({ departments }) => ({
705+
department: departments.id,
706+
}))
707+
.distinct(),
708+
})
709+
710+
// There are 3 distinct departments that have active users
711+
expect(distinctJoinedData.size).toBe(3)
712+
713+
const results = Array.from(distinctJoinedData.values())
714+
715+
// Should have distinct combinations of department
716+
const combinations = results.map((r) => `${r.department}`)
717+
const uniqueCombinations = [...new Set(combinations)]
718+
expect(combinations.length).toBe(uniqueCombinations.length)
719+
})
720+
})
549721
})
550722
}
551723

0 commit comments

Comments
 (0)