Skip to content

Commit e865acd

Browse files
committed
Unit test for deduplicating limited ordered queries in query collection
1 parent e181a06 commit e865acd

File tree

1 file changed

+148
-0
lines changed

1 file changed

+148
-0
lines changed

packages/query-db-collection/tests/query.test.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2943,5 +2943,153 @@ describe(`QueryCollection`, () => {
29432943
expect(collection.has(`3`)).toBe(false)
29442944
expect(collection.has(`4`)).toBe(false)
29452945
})
2946+
2947+
it(`should deduplicate queries and handle GC correctly when queries are ordered and have a LIMIT`, async () => {
2948+
const baseQueryKey = [`deduplication-gc-test`]
2949+
2950+
// Mock queryFn to return different data based on predicates
2951+
const queryFn = vi.fn().mockImplementation((context) => {
2952+
const { meta } = context
2953+
const loadSubsetOptions = meta?.loadSubsetOptions ?? {}
2954+
const { where, limit } = loadSubsetOptions
2955+
2956+
// Query 1: all items with category A (no limit)
2957+
if (
2958+
where?.name === `eq` &&
2959+
where?.args[0].path?.[0] === `category` &&
2960+
where?.args[1].value === `A` &&
2961+
!limit
2962+
) {
2963+
return Promise.resolve([
2964+
{ id: `1`, name: `Item 1`, category: `A` },
2965+
{ id: `2`, name: `Item 2`, category: `A` },
2966+
{ id: `3`, name: `Item 3`, category: `A` },
2967+
])
2968+
}
2969+
2970+
return Promise.resolve([])
2971+
})
2972+
2973+
const config: QueryCollectionConfig<CategorisedItem> = {
2974+
id: `deduplication-test`,
2975+
queryClient,
2976+
queryKey: (ctx) => {
2977+
const key = [...baseQueryKey]
2978+
if (ctx.where) {
2979+
key.push(`where`, JSON.stringify(ctx.where))
2980+
}
2981+
if (ctx.limit) {
2982+
key.push(`limit`, ctx.limit.toString())
2983+
}
2984+
if (ctx.orderBy) {
2985+
key.push(`orderBy`, JSON.stringify(ctx.orderBy))
2986+
}
2987+
return key
2988+
},
2989+
queryFn,
2990+
getKey,
2991+
startSync: true,
2992+
syncMode: `on-demand`,
2993+
}
2994+
2995+
const options = queryCollectionOptions(config)
2996+
const collection = createCollection(options)
2997+
2998+
// Collection should start empty with on-demand sync mode
2999+
expect(collection.size).toBe(0)
3000+
3001+
// Execute first query: load all rows that belong to category A (returns 3 rows)
3002+
const whereClause1 = new Func(`eq`, [
3003+
new PropRef([`category`]),
3004+
new Value(`A`),
3005+
])
3006+
await collection._sync.loadSubset({
3007+
where: whereClause1,
3008+
})
3009+
3010+
// Wait for first query data to load
3011+
await vi.waitFor(() => {
3012+
expect(collection.size).toBe(3)
3013+
expect(queryFn).toHaveBeenCalledTimes(1)
3014+
})
3015+
3016+
// Verify all 3 items are present
3017+
expect(collection.has(`1`)).toBe(true)
3018+
expect(collection.has(`2`)).toBe(true)
3019+
expect(collection.has(`3`)).toBe(true)
3020+
3021+
// Execute second query: load rows with category A, limit 2, ordered by ID
3022+
// This should be deduplicated since we already have all category A data
3023+
// So it will load the data from the local collection
3024+
const whereClause2 = new Func(`eq`, [
3025+
new PropRef([`category`]),
3026+
new Value(`A`),
3027+
])
3028+
await collection._sync.loadSubset({
3029+
where: whereClause2,
3030+
limit: 2,
3031+
orderBy: [
3032+
{
3033+
expression: new PropRef([`id`]),
3034+
compareOptions: {
3035+
direction: `asc`,
3036+
nulls: `last`,
3037+
stringSort: `lexical`,
3038+
},
3039+
},
3040+
],
3041+
})
3042+
3043+
// Second query should still only have been called once
3044+
// since query2 is deduplicated so it is executed against the local collection
3045+
// and not via queryFn
3046+
expect(queryFn).toHaveBeenCalledTimes(1)
3047+
3048+
// Collection should still have all 3 items (deduplication doesn't remove data)
3049+
expect(collection.size).toBe(3)
3050+
expect(collection.has(`1`)).toBe(true)
3051+
expect(collection.has(`2`)).toBe(true)
3052+
expect(collection.has(`3`)).toBe(true)
3053+
3054+
// GC the first query (all category A without limit)
3055+
queryClient.removeQueries({
3056+
queryKey: (config.queryKey as any)({ where: whereClause1 }),
3057+
exact: true,
3058+
})
3059+
3060+
// Wait for GC to process
3061+
await vi.waitFor(() => {
3062+
expect(collection.size).toBe(2) // Should only have items 1 and 2 because they are still referenced by query 2
3063+
})
3064+
3065+
// Verify that only row 3 is removed (it was only referenced by query 1)
3066+
expect(collection.has(`1`)).toBe(true) // Still present (referenced by query 2)
3067+
expect(collection.has(`2`)).toBe(true) // Still present (referenced by query 2)
3068+
expect(collection.has(`3`)).toBe(false) // Removed (only referenced by query 1)
3069+
3070+
// GC the second query (category A with limit 2)
3071+
queryClient.removeQueries({
3072+
queryKey: (config.queryKey as any)({
3073+
where: whereClause2,
3074+
limit: 2,
3075+
orderBy: [
3076+
{
3077+
expression: new PropRef([`id`]),
3078+
compareOptions: {
3079+
direction: `asc`,
3080+
nulls: `last`,
3081+
stringSort: `lexical`,
3082+
},
3083+
},
3084+
],
3085+
}),
3086+
exact: true,
3087+
})
3088+
3089+
// Wait for final GC to process
3090+
await vi.waitFor(() => {
3091+
expect(collection.size).toBe(0)
3092+
})
3093+
})
29463094
})
29473095
})

0 commit comments

Comments
 (0)