diff --git a/.changeset/twelve-jars-lead.md b/.changeset/twelve-jars-lead.md new file mode 100644 index 0000000000..3f3c73f838 --- /dev/null +++ b/.changeset/twelve-jars-lead.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db': patch +--- + +fix: preserve orderBy in includes after child collection update diff --git a/packages/db/src/query/live/collection-config-builder.ts b/packages/db/src/query/live/collection-config-builder.ts index 7353b2116f..e88cb87605 100644 --- a/packages/db/src/query/live/collection-config-builder.ts +++ b/packages/db/src/query/live/collection-config-builder.ts @@ -877,6 +877,9 @@ export class CollectionConfigBuilder< } else if (multiplicity > 0) { existing.inserts += multiplicity existing.value = childResult + if (_orderByIndex !== undefined) { + existing.orderByIndex = _orderByIndex + } } byChild.set(childKey, existing) @@ -1262,6 +1265,9 @@ function setupNestedPipelines( } else if (multiplicity > 0) { existing.inserts += multiplicity existing.value = childResult + if (_orderByIndex !== undefined) { + existing.orderByIndex = _orderByIndex + } } byChild.set(childKey, existing) diff --git a/packages/db/tests/query/includes.test.ts b/packages/db/tests/query/includes.test.ts index a0b490bdfe..f34d8b7551 100644 --- a/packages/db/tests/query/includes.test.ts +++ b/packages/db/tests/query/includes.test.ts @@ -5124,4 +5124,86 @@ describe(`includes subqueries`, () => { expect(data().textDeltas).toHaveLength(2) }) }) + + describe(`orderBy in includes after child collection update`, () => { + type Status = { id: number; name: string; position: number } + type Task = { id: number; statusId: number; name: string; position: number } + + it(`preserves child orderBy after optimistic update on child collection`, async () => { + const statusesOptions = mockSyncCollectionOptions({ + id: `orderby-includes-statuses`, + getKey: (s) => s.id, + initialData: [{ id: 1, name: `Todo`, position: 0 }], + }) + const statuses = createCollection(statusesOptions) + + const tasksOptions = mockSyncCollectionOptions({ + id: `orderby-includes-tasks`, + getKey: (t) => t.id, + initialData: [ + { id: 1, statusId: 1, name: `Hello`, position: 0 }, + { id: 2, statusId: 1, name: `World`, position: 1 }, + ], + }) + const tasks = createCollection(tasksOptions) + + const liveQuery = createLiveQueryCollection((q) => + q + .from({ status: statuses }) + .orderBy(({ status }) => status.position, `asc`) + .select(({ status }) => ({ + id: status.id, + name: status.name, + position: status.position, + tasks: q + .from({ task: tasks }) + .where(({ task }) => eq(task.statusId, status.id)) + .orderBy(({ task }) => task.position, `asc`) + .select(({ task }) => ({ + id: task.id, + name: task.name, + position: task.position, + })), + })), + ) + + await liveQuery.preload() + + type TaskResult = { id: number; name: string; position: number } + type StatusResult = { tasks: { toArray: Array } } + const getTaskOrder = () => + [...(liveQuery.get(1) as unknown as StatusResult).tasks.toArray].map( + (t) => t.id, + ) + + // Initial order: task 1 (pos=0) before task 2 (pos=1) + expect(getTaskOrder()).toEqual([1, 2]) + + // Optimistic update: swap positions + tasks.update(1, (draft) => { + draft.position = 1 + }) + tasks.update(2, (draft) => { + draft.position = 0 + }) + + // Immediately after optimistic update: task 2 (pos=0) should come first + expect(getTaskOrder()).toEqual([2, 1]) + + // Server confirms the same changes + tasksOptions.utils.begin() + tasksOptions.utils.write({ + type: `update`, + value: { id: 1, statusId: 1, name: `Hello`, position: 1 }, + }) + tasksOptions.utils.write({ + type: `update`, + value: { id: 2, statusId: 1, name: `World`, position: 0 }, + }) + tasksOptions.utils.commit() + + // After server confirmation: order should still be correct + expect(getTaskOrder()).toEqual([2, 1]) + }) + }) })