From fdd892bd0d9cb7fc02ef6661ac9a46fcaa461523 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 10 Dec 2025 10:51:39 +0000 Subject: [PATCH 1/7] feat: Add groupedOrderBy operator This commit introduces the `groupedOrderBy` operator, which allows for ordering and limiting elements within distinct groups. It also includes necessary exports and tests for this new functionality. Co-authored-by: kevin.de.porre --- .changeset/grouped-orderby-operator.md | 5 + .../db-ivm/src/operators/groupedOrderBy.ts | 95 +++ packages/db-ivm/src/operators/index.ts | 1 + .../groupedOrderByWithFractionalIndex.test.ts | 685 ++++++++++++++++++ packages/rxdb-db-collection/src/rxdb.ts | 4 +- 5 files changed, 788 insertions(+), 2 deletions(-) create mode 100644 .changeset/grouped-orderby-operator.md create mode 100644 packages/db-ivm/src/operators/groupedOrderBy.ts create mode 100644 packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts diff --git a/.changeset/grouped-orderby-operator.md b/.changeset/grouped-orderby-operator.md new file mode 100644 index 000000000..73f0f9415 --- /dev/null +++ b/.changeset/grouped-orderby-operator.md @@ -0,0 +1,5 @@ +--- +"@tanstack/db-ivm": patch +--- + +Add `groupedOrderByWithFractionalIndex` operator. This operator groups elements by a provided `groupKeyFn` and applies ordering and limits independently to each group. Each group maintains its own sorted collection with independent limit/offset, which is useful for hierarchical data projections where child collections need to enforce limits within each parent's slice of the stream rather than across the entire dataset. diff --git a/packages/db-ivm/src/operators/groupedOrderBy.ts b/packages/db-ivm/src/operators/groupedOrderBy.ts new file mode 100644 index 000000000..901e05c7c --- /dev/null +++ b/packages/db-ivm/src/operators/groupedOrderBy.ts @@ -0,0 +1,95 @@ +import { groupedTopKWithFractionalIndex } from './groupedTopKWithFractionalIndex.js' +import { consolidate } from './consolidate.js' +import type { IStreamBuilder, KeyValue } from '../types.js' + +export interface GroupedOrderByOptions { + comparator?: (a: Ve, b: Ve) => number + limit?: number + offset?: number +} + +export interface GroupedOrderByWithFractionalIndexOptions< + Ve, + KeyType = unknown, + ValueType = unknown, +> extends GroupedOrderByOptions { + setSizeCallback?: (getSize: () => number) => void + setWindowFn?: ( + windowFn: (options: { offset?: number; limit?: number }) => void, + ) => void + /** + * Function to extract a group key from the element's key and value. + * Elements with the same group key will be sorted and limited together. + */ + groupKeyFn: (key: KeyType, value: ValueType) => unknown +} + +/** + * Orders the elements per group and limits the number of results per group, with optional offset and + * annotates the value with a fractional index. + * This requires a keyed stream, and uses the `groupedTopKWithFractionalIndex` operator to order elements within each group. + * + * Elements are grouped by the provided groupKeyFn, and each group maintains its own sorted collection + * with independent limit/offset. + * + * @param valueExtractor - A function that extracts the value to order by from the element + * @param options - Configuration including groupKeyFn, comparator, limit, and offset + * @returns A piped operator that orders the elements per group and limits the number of results per group + */ +export function groupedOrderByWithFractionalIndex< + T extends KeyValue, + Ve = unknown, +>( + valueExtractor: ( + value: T extends KeyValue ? V : never, + ) => Ve, + options: GroupedOrderByWithFractionalIndexOptions< + Ve, + T extends KeyValue ? K : never, + T extends KeyValue ? V : never + >, +) { + type KeyType = T extends KeyValue ? K : never + type ValueType = T extends KeyValue ? V : never + + const limit = options.limit ?? Infinity + const offset = options.offset ?? 0 + const setSizeCallback = options.setSizeCallback + const setWindowFn = options.setWindowFn + const groupKeyFn = options.groupKeyFn + const comparator = + options.comparator ?? + ((a, b) => { + // Default to JS like ordering + if (a === b) return 0 + if (a < b) return -1 + return 1 + }) + + return ( + stream: IStreamBuilder, + ): IStreamBuilder<[KeyType, [ValueType, string]]> => { + // Cast to the expected key type for groupedTopKWithFractionalIndex + type StreamKey = KeyType extends string | number ? KeyType : string | number + + return stream.pipe( + groupedTopKWithFractionalIndex( + (a: ValueType, b: ValueType) => + comparator(valueExtractor(a), valueExtractor(b)), + { + limit, + offset, + setSizeCallback, + setWindowFn, + groupKeyFn: groupKeyFn as ( + key: StreamKey, + value: ValueType, + ) => unknown, + }, + ) as ( + stream: IStreamBuilder, + ) => IStreamBuilder<[KeyType, [ValueType, string]]>, + consolidate(), + ) + } +} diff --git a/packages/db-ivm/src/operators/index.ts b/packages/db-ivm/src/operators/index.ts index 3f8ab8cb8..13acf130b 100644 --- a/packages/db-ivm/src/operators/index.ts +++ b/packages/db-ivm/src/operators/index.ts @@ -16,5 +16,6 @@ export * from './topK.js' export * from './topKWithFractionalIndex.js' export * from './groupedTopKWithFractionalIndex.js' export * from './orderBy.js' +export * from './groupedOrderBy.js' export * from './filterBy.js' export { groupBy, groupByOperators } from './groupBy.js' diff --git a/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts b/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts new file mode 100644 index 000000000..dd5fe52d1 --- /dev/null +++ b/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts @@ -0,0 +1,685 @@ +import { describe, expect, it } from 'vitest' +import { D2 } from '../../src/d2.js' +import { MultiSet } from '../../src/multiset.js' +import { groupedOrderByWithFractionalIndex } from '../../src/operators/groupedOrderBy.js' +import { output } from '../../src/operators/index.js' +import { MessageTracker, compareFractionalIndex } from '../test-utils.js' +import type { KeyValue } from '../../src/types.js' + +describe(`Operators`, () => { + describe(`GroupedOrderByWithFractionalIndex operator`, () => { + it(`should maintain separate ordering per group with array key`, () => { + const graph = new D2() + const input = graph.newInput< + KeyValue< + [string, string], + { + id: string + value: number + } + > + >() + const tracker = new MessageTracker< + [[string, string], [{ id: string; value: number }, string]] + >() + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.value, { + limit: 2, + groupKeyFn: (key, _value) => key[0], + }), + output((message) => { + tracker.addMessage(message) + }), + ) + + graph.finalize() + + input.sendData( + new MultiSet([ + [[[`group1`, `a`], { id: `g1-a`, value: 5 }], 1], + [[[`group1`, `b`], { id: `g1-b`, value: 1 }], 1], + [[[`group1`, `c`], { id: `g1-c`, value: 3 }], 1], + [[[`group2`, `a`], { id: `g2-a`, value: 4 }], 1], + [[[`group2`, `b`], { id: `g2-b`, value: 2 }], 1], + [[[`group2`, `c`], { id: `g2-c`, value: 6 }], 1], + ]), + ) + + graph.run() + + const result = tracker.getResult(compareFractionalIndex) + + // Each group should have limit 2, so 4 total results + expect(result.sortedResults.length).toBe(4) + + // Group by group key and verify each group's results + const groupedValues = new Map>() + for (const [key, [value, _index]] of result.sortedResults) { + // key is [string, string], extract the first element as the group + const group = (key)[0] + const list = groupedValues.get(group) ?? [] + list.push(value.value) + groupedValues.set(group, list) + } + + // Sort values within each group for consistent comparison + for (const [group, values] of groupedValues) { + values.sort((a, b) => a - b) + groupedValues.set(group, values) + } + + // group1 should have values 1, 3 (top 2 by ascending value) + expect(groupedValues.get(`group1`)).toEqual([1, 3]) + // group2 should have values 2, 4 (top 2 by ascending value) + expect(groupedValues.get(`group2`)).toEqual([2, 4]) + }) + + it(`should group by value property using groupKeyFn`, () => { + const graph = new D2() + const input = + graph.newInput< + KeyValue + >() + const tracker = new MessageTracker< + [string, [{ id: string; group: string; value: number }, string]] + >() + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.value, { + limit: 2, + groupKeyFn: (_key, value) => value.group, + }), + output((message) => { + tracker.addMessage(message) + }), + ) + + graph.finalize() + + // Initial data - 3 items per group + input.sendData( + new MultiSet([ + [[`g1-a`, { id: `g1-a`, group: `group1`, value: 5 }], 1], + [[`g1-b`, { id: `g1-b`, group: `group1`, value: 1 }], 1], + [[`g1-c`, { id: `g1-c`, group: `group1`, value: 3 }], 1], + [[`g2-a`, { id: `g2-a`, group: `group2`, value: 4 }], 1], + [[`g2-b`, { id: `g2-b`, group: `group2`, value: 2 }], 1], + [[`g2-c`, { id: `g2-c`, group: `group2`, value: 6 }], 1], + ]), + ) + graph.run() + + const result = tracker.getResult(compareFractionalIndex) + + // Each group should have limit 2, so 4 total results + expect(result.sortedResults.length).toBe(4) + + // Group by group key and verify each group's results + const groupedValues = new Map>() + for (const [_key, [value, _index]] of result.sortedResults) { + const group = value.group + const list = groupedValues.get(group) ?? [] + list.push(value.value) + groupedValues.set(group, list) + } + + // Sort values within each group for consistent comparison + for (const [group, values] of groupedValues) { + values.sort((a, b) => a - b) + groupedValues.set(group, values) + } + + // group1 should have values 1, 3 (top 2 by ascending value) + expect(groupedValues.get(`group1`)).toEqual([1, 3]) + // group2 should have values 2, 4 (top 2 by ascending value) + expect(groupedValues.get(`group2`)).toEqual([2, 4]) + }) + + it(`should handle incremental updates within a group`, () => { + const graph = new D2() + const input = + graph.newInput< + KeyValue + >() + const tracker = new MessageTracker< + [string, [{ id: string; group: string; value: number }, string]] + >() + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.value, { + limit: 2, + groupKeyFn: (_key, value) => value.group, + }), + output((message) => { + tracker.addMessage(message) + }), + ) + + graph.finalize() + + // Initial data + input.sendData( + new MultiSet([ + [[`g1-a`, { id: `g1-a`, group: `group1`, value: 5 }], 1], + [[`g1-b`, { id: `g1-b`, group: `group1`, value: 1 }], 1], + [[`g1-c`, { id: `g1-c`, group: `group1`, value: 3 }], 1], + ]), + ) + graph.run() + + // Initial should have 2 items (limit 2): values 1 and 3 + const initialResult = tracker.getResult(compareFractionalIndex) + expect(initialResult.sortedResults.length).toBe(2) + const initialValues = initialResult.sortedResults + .map(([_key, [value, _index]]) => value.value) + .sort((a, b) => a - b) + expect(initialValues).toEqual([1, 3]) + + const initialMessageCount = initialResult.messageCount + + // Insert a better value (0) which should evict value 3 + input.sendData( + new MultiSet([ + [[`g1-d`, { id: `g1-d`, group: `group1`, value: 0 }], 1], + ]), + ) + graph.run() + + const updateResult = tracker.getResult(compareFractionalIndex) + // Should have 2 new messages: add 0, remove 3 + expect(updateResult.messageCount - initialMessageCount).toBe(2) + + // Check final state (cumulative) + const finalValues = updateResult.sortedResults + .map(([_key, [value, _index]]) => value.value) + .sort((a, b) => a - b) + expect(finalValues).toEqual([0, 1]) + }) + + it(`should handle removal of elements from topK`, () => { + const graph = new D2() + const input = + graph.newInput< + KeyValue + >() + const tracker = new MessageTracker< + [string, [{ id: string; group: string; value: number }, string]] + >() + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.value, { + limit: 2, + groupKeyFn: (_key, value) => value.group, + }), + output((message) => { + tracker.addMessage(message) + }), + ) + + graph.finalize() + + // Initial data + input.sendData( + new MultiSet([ + [[`g1-a`, { id: `g1-a`, group: `group1`, value: 5 }], 1], + [[`g1-b`, { id: `g1-b`, group: `group1`, value: 1 }], 1], + [[`g1-c`, { id: `g1-c`, group: `group1`, value: 3 }], 1], + ]), + ) + graph.run() + + const initialMessageCount = tracker.getResult().messageCount + + // Remove the element with value 1 (which is in topK) + input.sendData( + new MultiSet([ + [[`g1-b`, { id: `g1-b`, group: `group1`, value: 1 }], -1], + ]), + ) + graph.run() + + const updateResult = tracker.getResult(compareFractionalIndex) + // Should have 2 new messages: remove 1, add 5 + expect(updateResult.messageCount - initialMessageCount).toBe(2) + + // Final state should have values 3 and 5 + const finalValues = updateResult.sortedResults + .map(([_key, [value, _index]]) => value.value) + .sort((a, b) => a - b) + expect(finalValues).toEqual([3, 5]) + }) + + it(`should handle multiple groups independently`, () => { + const graph = new D2() + const input = + graph.newInput< + KeyValue + >() + const tracker = new MessageTracker< + [string, [{ id: string; group: string; value: number }, string]] + >() + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.value, { + limit: 2, + groupKeyFn: (_key, value) => value.group, + }), + output((message) => { + tracker.addMessage(message) + }), + ) + + graph.finalize() + + // Initial data for two groups + input.sendData( + new MultiSet([ + [[`g1-a`, { id: `g1-a`, group: `group1`, value: 10 }], 1], + [[`g1-b`, { id: `g1-b`, group: `group1`, value: 20 }], 1], + [[`g2-a`, { id: `g2-a`, group: `group2`, value: 5 }], 1], + [[`g2-b`, { id: `g2-b`, group: `group2`, value: 15 }], 1], + ]), + ) + graph.run() + + tracker.reset() + + // Update only group1 - add a better value + input.sendData( + new MultiSet([ + [[`g1-c`, { id: `g1-c`, group: `group1`, value: 5 }], 1], + ]), + ) + graph.run() + + const updateResult = tracker.getResult() + + // Should have exactly 2 messages: one removal and one addition + expect(updateResult.messages.length).toBe(2) + + // Find the removal message (multiplicity -1) and addition message (multiplicity 1) + const removalMessage = updateResult.messages.find( + ([_item, mult]) => mult === -1, + ) + const additionMessage = updateResult.messages.find( + ([_item, mult]) => mult === 1, + ) + + expect(removalMessage).toBeDefined() + expect(additionMessage).toBeDefined() + + // Check that removal is for value 20 (g1-b) + const [_removalKey, [removalValue, _removalIdx]] = removalMessage![0] + expect(removalValue.value).toBe(20) + expect(removalValue.id).toBe(`g1-b`) + + // Check that addition is for value 5 (g1-c) + const [_additionKey, [additionValue, _additionIdx]] = additionMessage![0] + expect(additionValue.value).toBe(5) + expect(additionValue.id).toBe(`g1-c`) + }) + + it(`should support offset within groups`, () => { + const graph = new D2() + const input = + graph.newInput< + KeyValue + >() + const tracker = new MessageTracker< + [string, [{ id: string; group: string; value: number }, string]] + >() + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.value, { + limit: 2, + offset: 1, + groupKeyFn: (_key, value) => value.group, + }), + output((message) => { + tracker.addMessage(message) + }), + ) + + graph.finalize() + + // Initial data - 4 items per group + input.sendData( + new MultiSet([ + [[`g1-a`, { id: `g1-a`, group: `group1`, value: 1 }], 1], + [[`g1-b`, { id: `g1-b`, group: `group1`, value: 2 }], 1], + [[`g1-c`, { id: `g1-c`, group: `group1`, value: 3 }], 1], + [[`g1-d`, { id: `g1-d`, group: `group1`, value: 4 }], 1], + ]), + ) + graph.run() + + const result = tracker.getResult(compareFractionalIndex) + + // With offset 1 and limit 2, should get values 2 and 3 + const values = result.sortedResults + .map(([_key, [value, _index]]) => value.value) + .sort((a, b) => a - b) + expect(values).toEqual([2, 3]) + }) + + it(`should use custom comparator`, () => { + const graph = new D2() + const input = + graph.newInput< + KeyValue + >() + const tracker = new MessageTracker< + [string, [{ id: string; group: string; value: number }, string]] + >() + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.value, { + limit: 2, + groupKeyFn: (_key, value) => value.group, + // Descending order + comparator: (a, b) => b - a, + }), + output((message) => { + tracker.addMessage(message) + }), + ) + + graph.finalize() + + input.sendData( + new MultiSet([ + [[`g1-a`, { id: `g1-a`, group: `group1`, value: 1 }], 1], + [[`g1-b`, { id: `g1-b`, group: `group1`, value: 2 }], 1], + [[`g1-c`, { id: `g1-c`, group: `group1`, value: 3 }], 1], + ]), + ) + graph.run() + + const result = tracker.getResult(compareFractionalIndex) + + // With descending order and limit 2, should get values 3 and 2 + const values = result.sortedResults + .map(([_key, [value, _index]]) => value.value) + .sort((a, b) => b - a) // Sort descending for comparison + expect(values).toEqual([3, 2]) + }) + + it(`should use groupKeyFn to extract group from key with delimiter`, () => { + const graph = new D2() + // Use keys with format "group:itemId" + const input = + graph.newInput>() + const tracker = new MessageTracker< + [string, [{ id: string; value: number }, string]] + >() + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.value, { + limit: 2, + // Extract group from key "group:itemId" + groupKeyFn: (key, _value) => key.split(`:`)[0], + }), + output((message) => { + tracker.addMessage(message) + }), + ) + + graph.finalize() + + input.sendData( + new MultiSet([ + [[`group1:a`, { id: `g1-a`, value: 5 }], 1], + [[`group1:b`, { id: `g1-b`, value: 1 }], 1], + [[`group1:c`, { id: `g1-c`, value: 3 }], 1], + [[`group2:a`, { id: `g2-a`, value: 4 }], 1], + [[`group2:b`, { id: `g2-b`, value: 2 }], 1], + [[`group2:c`, { id: `g2-c`, value: 6 }], 1], + ]), + ) + graph.run() + + const result = tracker.getResult(compareFractionalIndex) + + // Group results by group extracted from key + const groupedValues = new Map>() + for (const [key, [value, _index]] of result.sortedResults) { + const group = key.split(`:`)[0]! + const list = groupedValues.get(group) ?? [] + list.push(value.value) + groupedValues.set(group, list) + } + + for (const [group, values] of groupedValues) { + values.sort((a, b) => a - b) + groupedValues.set(group, values) + } + + expect(groupedValues.get(`group1`)).toEqual([1, 3]) + expect(groupedValues.get(`group2`)).toEqual([2, 4]) + }) + + it(`should support infinite limit (no limit)`, () => { + const graph = new D2() + const input = + graph.newInput< + KeyValue + >() + const tracker = new MessageTracker< + [string, [{ id: string; group: string; value: number }, string]] + >() + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.value, { + // No limit specified - defaults to Infinity + groupKeyFn: (_key, value) => value.group, + }), + output((message) => { + tracker.addMessage(message) + }), + ) + + graph.finalize() + + input.sendData( + new MultiSet([ + [[`g1-a`, { id: `g1-a`, group: `group1`, value: 5 }], 1], + [[`g1-b`, { id: `g1-b`, group: `group1`, value: 1 }], 1], + [[`g1-c`, { id: `g1-c`, group: `group1`, value: 3 }], 1], + ]), + ) + graph.run() + + const result = tracker.getResult(compareFractionalIndex) + + // All 3 items should be in the result + expect(result.sortedResults.length).toBe(3) + const values = result.sortedResults + .map(([_key, [value, _index]]) => value.value) + .sort((a, b) => a - b) + expect(values).toEqual([1, 3, 5]) + }) + + it(`should handle setSizeCallback correctly`, () => { + const graph = new D2() + const input = + graph.newInput< + KeyValue + >() + let getSize: (() => number) | undefined + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.value, { + limit: 2, + groupKeyFn: (_key, value) => value.group, + setSizeCallback: (fn) => { + getSize = fn + }, + }), + output(() => {}), + ) + + graph.finalize() + + expect(getSize).toBeDefined() + expect(getSize!()).toBe(0) // Initially empty + + input.sendData( + new MultiSet([ + [[`g1-a`, { id: `g1-a`, group: `group1`, value: 5 }], 1], + [[`g1-b`, { id: `g1-b`, group: `group1`, value: 1 }], 1], + [[`g1-c`, { id: `g1-c`, group: `group1`, value: 3 }], 1], + [[`g2-a`, { id: `g2-a`, group: `group2`, value: 4 }], 1], + [[`g2-b`, { id: `g2-b`, group: `group2`, value: 2 }], 1], + ]), + ) + graph.run() + + // group1 has 2 items in topK, group2 has 2 items + expect(getSize!()).toBe(4) + }) + + it(`should handle moving window with setWindowFn`, () => { + const graph = new D2() + const input = + graph.newInput< + KeyValue + >() + const tracker = new MessageTracker< + [string, [{ id: string; group: string; value: number }, string]] + >() + let windowFn: + | ((options: { offset?: number; limit?: number }) => void) + | undefined + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.value, { + limit: 2, + offset: 0, + groupKeyFn: (_key, value) => value.group, + setWindowFn: (fn) => { + windowFn = fn + }, + }), + output((message) => { + tracker.addMessage(message) + }), + ) + + graph.finalize() + + input.sendData( + new MultiSet([ + [[`g1-a`, { id: `g1-a`, group: `group1`, value: 1 }], 1], + [[`g1-b`, { id: `g1-b`, group: `group1`, value: 2 }], 1], + [[`g1-c`, { id: `g1-c`, group: `group1`, value: 3 }], 1], + [[`g1-d`, { id: `g1-d`, group: `group1`, value: 4 }], 1], + ]), + ) + graph.run() + + // Initial: values 1, 2 + const initialResult = tracker.getResult(compareFractionalIndex) + const initialValues = initialResult.sortedResults + .map(([_key, [value, _index]]) => value.value) + .sort((a, b) => a - b) + expect(initialValues).toEqual([1, 2]) + + // Move window to offset 1 + windowFn!({ offset: 1 }) + graph.run() + + // Now should have values 2, 3 + const movedResult = tracker.getResult(compareFractionalIndex) + const movedValues = movedResult.sortedResults + .map(([_key, [value, _index]]) => value.value) + .sort((a, b) => a - b) + expect(movedValues).toEqual([2, 3]) + }) + + it(`should cleanup empty groups`, () => { + const graph = new D2() + const input = + graph.newInput< + KeyValue + >() + const tracker = new MessageTracker< + [string, [{ id: string; group: string; value: number }, string]] + >() + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.value, { + limit: 2, + groupKeyFn: (_key, value) => value.group, + }), + output((message) => { + tracker.addMessage(message) + }), + ) + + graph.finalize() + + // Add items to two groups + input.sendData( + new MultiSet([ + [[`g1-a`, { id: `g1-a`, group: `group1`, value: 1 }], 1], + [[`g2-a`, { id: `g2-a`, group: `group2`, value: 2 }], 1], + ]), + ) + graph.run() + + expect(tracker.getResult().sortedResults.length).toBe(2) + + // Remove all items from group1 + input.sendData( + new MultiSet([ + [[`g1-a`, { id: `g1-a`, group: `group1`, value: 1 }], -1], + ]), + ) + graph.run() + + // Should have only group2 left in materialized results + const updateResult = tracker.getResult(compareFractionalIndex) + expect(updateResult.sortedResults.length).toBe(1) + expect(updateResult.sortedResults[0]![1][0].group).toBe(`group2`) + }) + + it(`should order by string property`, () => { + const graph = new D2() + const input = + graph.newInput< + KeyValue + >() + const tracker = new MessageTracker< + [string, [{ id: string; group: string; name: string }, string]] + >() + + input.pipe( + groupedOrderByWithFractionalIndex((item) => item.name, { + limit: 2, + groupKeyFn: (_key, value) => value.group, + }), + output((message) => { + tracker.addMessage(message) + }), + ) + + graph.finalize() + + input.sendData( + new MultiSet([ + [[`g1-a`, { id: `g1-a`, group: `group1`, name: `charlie` }], 1], + [[`g1-b`, { id: `g1-b`, group: `group1`, name: `alice` }], 1], + [[`g1-c`, { id: `g1-c`, group: `group1`, name: `bob` }], 1], + ]), + ) + graph.run() + + const result = tracker.getResult(compareFractionalIndex) + const names = result.sortedResults + .map(([_key, [value, _index]]) => value.name) + .sort() + expect(names).toEqual([`alice`, `bob`]) + }) + }) +}) diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index 6ec59cfb6..28d83aa03 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -280,7 +280,7 @@ export function rxdbCollectionOptions(config: RxDBCollectionConfig) { onUpdate: async (params) => { debug(`update`, params) const mutations = params.transaction.mutations.filter( - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (m) => m.type === `update`, ) @@ -297,7 +297,7 @@ export function rxdbCollectionOptions(config: RxDBCollectionConfig) { onDelete: async (params) => { debug(`delete`, params) const mutations = params.transaction.mutations.filter( - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (m) => m.type === `delete`, ) const ids = mutations.map((mutation) => getKey(mutation.original)) From 276332ee1c270348b07928639c80d790b3b3b3fa Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:53:59 +0000 Subject: [PATCH 2/7] ci: apply automated fixes --- .changeset/grouped-orderby-operator.md | 2 +- .../tests/operators/groupedOrderByWithFractionalIndex.test.ts | 2 +- packages/rxdb-db-collection/src/rxdb.ts | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.changeset/grouped-orderby-operator.md b/.changeset/grouped-orderby-operator.md index 73f0f9415..f09e5bd10 100644 --- a/.changeset/grouped-orderby-operator.md +++ b/.changeset/grouped-orderby-operator.md @@ -1,5 +1,5 @@ --- -"@tanstack/db-ivm": patch +'@tanstack/db-ivm': patch --- Add `groupedOrderByWithFractionalIndex` operator. This operator groups elements by a provided `groupKeyFn` and applies ordering and limits independently to each group. Each group maintains its own sorted collection with independent limit/offset, which is useful for hierarchical data projections where child collections need to enforce limits within each parent's slice of the stream rather than across the entire dataset. diff --git a/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts b/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts index dd5fe52d1..eb05951c9 100644 --- a/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts +++ b/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts @@ -57,7 +57,7 @@ describe(`Operators`, () => { const groupedValues = new Map>() for (const [key, [value, _index]] of result.sortedResults) { // key is [string, string], extract the first element as the group - const group = (key)[0] + const group = key[0] const list = groupedValues.get(group) ?? [] list.push(value.value) groupedValues.set(group, list) diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index 28d83aa03..23880c534 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -280,7 +280,6 @@ export function rxdbCollectionOptions(config: RxDBCollectionConfig) { onUpdate: async (params) => { debug(`update`, params) const mutations = params.transaction.mutations.filter( - (m) => m.type === `update`, ) @@ -297,7 +296,6 @@ export function rxdbCollectionOptions(config: RxDBCollectionConfig) { onDelete: async (params) => { debug(`delete`, params) const mutations = params.transaction.mutations.filter( - (m) => m.type === `delete`, ) const ids = mutations.map((mutation) => getKey(mutation.original)) From e12c558c12960f52355d03ebdf4a3da604cc6d81 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 10 Dec 2025 13:35:31 +0100 Subject: [PATCH 3/7] Improve unit tests --- .../groupedOrderByWithFractionalIndex.test.ts | 219 +++++++++++------- 1 file changed, 137 insertions(+), 82 deletions(-) diff --git a/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts b/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts index eb05951c9..850d0c99a 100644 --- a/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts +++ b/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts @@ -21,12 +21,14 @@ describe(`Operators`, () => { >() const tracker = new MessageTracker< [[string, string], [{ id: string; value: number }, string]] - >() + >() + + const groupKeyFn = (key: [string, string]) => key[0] input.pipe( groupedOrderByWithFractionalIndex((item) => item.value, { limit: 2, - groupKeyFn: (key, _value) => key[0], + groupKeyFn, }), output((message) => { tracker.addMessage(message) @@ -53,25 +55,13 @@ describe(`Operators`, () => { // Each group should have limit 2, so 4 total results expect(result.sortedResults.length).toBe(4) - // Group by group key and verify each group's results - const groupedValues = new Map>() - for (const [key, [value, _index]] of result.sortedResults) { - // key is [string, string], extract the first element as the group - const group = key[0] - const list = groupedValues.get(group) ?? [] - list.push(value.value) - groupedValues.set(group, list) - } - - // Sort values within each group for consistent comparison - for (const [group, values] of groupedValues) { - values.sort((a, b) => a - b) - groupedValues.set(group, values) - } - - // group1 should have values 1, 3 (top 2 by ascending value) + // Sort all results by fractional index first, then group by group key + const sortedResults = sortByKeyAndIndex(result.sortedResults) + const groupedValues = groupResultsByKey(sortedResults, groupKeyFn) + + // group1 should have values 1, 3 (top 2 by ascending value, ordered by fractional index) expect(groupedValues.get(`group1`)).toEqual([1, 3]) - // group2 should have values 2, 4 (top 2 by ascending value) + // group2 should have values 2, 4 (top 2 by ascending value, ordered by fractional index) expect(groupedValues.get(`group2`)).toEqual([2, 4]) }) @@ -85,10 +75,12 @@ describe(`Operators`, () => { [string, [{ id: string; group: string; value: number }, string]] >() + const groupKeyFn = (_key: string, value: { id: string; group: string; value: number }) => value.group + input.pipe( groupedOrderByWithFractionalIndex((item) => item.value, { limit: 2, - groupKeyFn: (_key, value) => value.group, + groupKeyFn, }), output((message) => { tracker.addMessage(message) @@ -102,9 +94,9 @@ describe(`Operators`, () => { new MultiSet([ [[`g1-a`, { id: `g1-a`, group: `group1`, value: 5 }], 1], [[`g1-b`, { id: `g1-b`, group: `group1`, value: 1 }], 1], - [[`g1-c`, { id: `g1-c`, group: `group1`, value: 3 }], 1], [[`g2-a`, { id: `g2-a`, group: `group2`, value: 4 }], 1], [[`g2-b`, { id: `g2-b`, group: `group2`, value: 2 }], 1], + [[`g1-c`, { id: `g1-c`, group: `group1`, value: 3 }], 1], [[`g2-c`, { id: `g2-c`, group: `group2`, value: 6 }], 1], ]), ) @@ -115,24 +107,13 @@ describe(`Operators`, () => { // Each group should have limit 2, so 4 total results expect(result.sortedResults.length).toBe(4) - // Group by group key and verify each group's results - const groupedValues = new Map>() - for (const [_key, [value, _index]] of result.sortedResults) { - const group = value.group - const list = groupedValues.get(group) ?? [] - list.push(value.value) - groupedValues.set(group, list) - } - - // Sort values within each group for consistent comparison - for (const [group, values] of groupedValues) { - values.sort((a, b) => a - b) - groupedValues.set(group, values) - } - - // group1 should have values 1, 3 (top 2 by ascending value) + // Sort all results by fractional index first, then group by group key + const sortedResults = sortByKeyAndIndex(result.sortedResults) + const groupedValues = groupResultsByKey(sortedResults, groupKeyFn) + + // group1 should have values 1, 3 (top 2 by ascending value, ordered by fractional index) expect(groupedValues.get(`group1`)).toEqual([1, 3]) - // group2 should have values 2, 4 (top 2 by ascending value) + // group2 should have values 2, 4 (top 2 by ascending value, ordered by fractional index) expect(groupedValues.get(`group2`)).toEqual([2, 4]) }) @@ -171,9 +152,10 @@ describe(`Operators`, () => { // Initial should have 2 items (limit 2): values 1 and 3 const initialResult = tracker.getResult(compareFractionalIndex) expect(initialResult.sortedResults.length).toBe(2) - const initialValues = initialResult.sortedResults - .map(([_key, [value, _index]]) => value.value) - .sort((a, b) => a - b) + const sortedInitialResults = sortByKeyAndIndex(initialResult.sortedResults) + const initialValues = sortedInitialResults.map( + ([_key, [value, _index]]) => value.value, + ) expect(initialValues).toEqual([1, 3]) const initialMessageCount = initialResult.messageCount @@ -191,9 +173,10 @@ describe(`Operators`, () => { expect(updateResult.messageCount - initialMessageCount).toBe(2) // Check final state (cumulative) - const finalValues = updateResult.sortedResults - .map(([_key, [value, _index]]) => value.value) - .sort((a, b) => a - b) + const sortedFinalResults = sortByKeyAndIndex(updateResult.sortedResults) + const finalValues = sortedFinalResults.map( + ([_key, [value, _index]]) => value.value, + ) expect(finalValues).toEqual([0, 1]) }) @@ -244,9 +227,10 @@ describe(`Operators`, () => { expect(updateResult.messageCount - initialMessageCount).toBe(2) // Final state should have values 3 and 5 - const finalValues = updateResult.sortedResults - .map(([_key, [value, _index]]) => value.value) - .sort((a, b) => a - b) + const sortedFinalResults = sortByKeyAndIndex(updateResult.sortedResults) + const finalValues = sortedFinalResults.map( + ([_key, [value, _index]]) => value.value, + ) expect(finalValues).toEqual([3, 5]) }) @@ -260,10 +244,12 @@ describe(`Operators`, () => { [string, [{ id: string; group: string; value: number }, string]] >() + const groupKeyFn = (_key: string, value: { id: string; group: string; value: number }) => value.group + input.pipe( groupedOrderByWithFractionalIndex((item) => item.value, { limit: 2, - groupKeyFn: (_key, value) => value.group, + groupKeyFn, }), output((message) => { tracker.addMessage(message) @@ -283,6 +269,19 @@ describe(`Operators`, () => { ) graph.run() + // Check initial output: each group should have limit 2 + const initialResult = tracker.getResult(compareFractionalIndex) + expect(initialResult.sortedResults.length).toBe(4) + + // Sort all results by fractional index first, then group by group key + const sortedInitialResults = sortByKeyAndIndex(initialResult.sortedResults) + const initialGroupedValues = groupResultsByKey(sortedInitialResults, groupKeyFn) + + // group1 should have values 10, 20 (top 2 by ascending value, ordered by fractional index) + expect(initialGroupedValues.get(`group1`)).toEqual([10, 20]) + // group2 should have values 5, 15 (top 2 by ascending value, ordered by fractional index) + expect(initialGroupedValues.get(`group2`)).toEqual([5, 15]) + tracker.reset() // Update only group1 - add a better value @@ -315,9 +314,18 @@ describe(`Operators`, () => { expect(removalValue.id).toBe(`g1-b`) // Check that addition is for value 5 (g1-c) - const [_additionKey, [additionValue, _additionIdx]] = additionMessage![0] + const [_additionKey, [additionValue, additionIdx]] = additionMessage![0] expect(additionValue.value).toBe(5) expect(additionValue.id).toBe(`g1-c`) + + // Check that the fractional index of the added value (5) is smaller than the index of value 10 + const finalResult = tracker.getResult(compareFractionalIndex) + const value10Entry = finalResult.sortedResults.find( + ([_key, [value, _index]]) => value.value === 10 && value.group === `group1`, + ) + expect(value10Entry).toBeDefined() + const [_value10Key, [_value10Value, value10Idx]] = value10Entry! + expect(additionIdx < value10Idx).toBe(true) }) it(`should support offset within groups`, () => { @@ -357,9 +365,8 @@ describe(`Operators`, () => { const result = tracker.getResult(compareFractionalIndex) // With offset 1 and limit 2, should get values 2 and 3 - const values = result.sortedResults - .map(([_key, [value, _index]]) => value.value) - .sort((a, b) => a - b) + const sortedResults = sortByKeyAndIndex(result.sortedResults) + const values = sortedResults.map(([_key, [value, _index]]) => value.value) expect(values).toEqual([2, 3]) }) @@ -399,9 +406,8 @@ describe(`Operators`, () => { const result = tracker.getResult(compareFractionalIndex) // With descending order and limit 2, should get values 3 and 2 - const values = result.sortedResults - .map(([_key, [value, _index]]) => value.value) - .sort((a, b) => b - a) // Sort descending for comparison + const sortedResults = sortByKeyAndIndex(result.sortedResults) + const values = sortedResults.map(([_key, [value, _index]]) => value.value) expect(values).toEqual([3, 2]) }) @@ -414,11 +420,13 @@ describe(`Operators`, () => { [string, [{ id: string; value: number }, string]] >() + const groupKeyFn = (key: string, _value: { id: string; value: number }) => key.split(`:`)[0]! + input.pipe( groupedOrderByWithFractionalIndex((item) => item.value, { limit: 2, // Extract group from key "group:itemId" - groupKeyFn: (key, _value) => key.split(`:`)[0], + groupKeyFn, }), output((message) => { tracker.addMessage(message) @@ -441,19 +449,9 @@ describe(`Operators`, () => { const result = tracker.getResult(compareFractionalIndex) - // Group results by group extracted from key - const groupedValues = new Map>() - for (const [key, [value, _index]] of result.sortedResults) { - const group = key.split(`:`)[0]! - const list = groupedValues.get(group) ?? [] - list.push(value.value) - groupedValues.set(group, list) - } - - for (const [group, values] of groupedValues) { - values.sort((a, b) => a - b) - groupedValues.set(group, values) - } + // Sort all results by fractional index first, then group by group key + const sortedResults = sortByKeyAndIndex(result.sortedResults) + const groupedValues = groupResultsByKey(sortedResults, groupKeyFn) expect(groupedValues.get(`group1`)).toEqual([1, 3]) expect(groupedValues.get(`group2`)).toEqual([2, 4]) @@ -494,9 +492,8 @@ describe(`Operators`, () => { // All 3 items should be in the result expect(result.sortedResults.length).toBe(3) - const values = result.sortedResults - .map(([_key, [value, _index]]) => value.value) - .sort((a, b) => a - b) + const sortedResults = sortByKeyAndIndex(result.sortedResults) + const values = sortedResults.map(([_key, [value, _index]]) => value.value) expect(values).toEqual([1, 3, 5]) }) @@ -580,9 +577,10 @@ describe(`Operators`, () => { // Initial: values 1, 2 const initialResult = tracker.getResult(compareFractionalIndex) - const initialValues = initialResult.sortedResults - .map(([_key, [value, _index]]) => value.value) - .sort((a, b) => a - b) + const sortedInitialResults = sortByKeyAndIndex(initialResult.sortedResults) + const initialValues = sortedInitialResults.map( + ([_key, [value, _index]]) => value.value, + ) expect(initialValues).toEqual([1, 2]) // Move window to offset 1 @@ -591,9 +589,10 @@ describe(`Operators`, () => { // Now should have values 2, 3 const movedResult = tracker.getResult(compareFractionalIndex) - const movedValues = movedResult.sortedResults - .map(([_key, [value, _index]]) => value.value) - .sort((a, b) => a - b) + const sortedMovedResults = sortByKeyAndIndex(movedResult.sortedResults) + const movedValues = sortedMovedResults.map( + ([_key, [value, _index]]) => value.value, + ) expect(movedValues).toEqual([2, 3]) }) @@ -676,10 +675,66 @@ describe(`Operators`, () => { graph.run() const result = tracker.getResult(compareFractionalIndex) - const names = result.sortedResults - .map(([_key, [value, _index]]) => value.name) - .sort() + const sortedResults = sortByKeyAndIndex(result.sortedResults) + const names = sortedResults.map(([_key, [value, _index]]) => value.name) expect(names).toEqual([`alice`, `bob`]) }) }) }) + +/** + * Helper function to sort results by key and then index + */ +function sortByKeyAndIndex(results: Array) { + return [...results] + .sort( + ( + [[_aKey, [_aValue, _aIndex]], aMultiplicity], + [[_bKey, [_bValue, _bIndex]], bMultiplicity], + ) => aMultiplicity - bMultiplicity, + ) + .sort( + ( + [[aKey, [_aValue, _aIndex]], _aMultiplicity], + [[bKey, [_bValue, _bIndex]], _bMultiplicity], + ) => { + // Compare keys - handle string, array, and numeric keys + if (typeof aKey === 'number' && typeof bKey === 'number') { + return aKey - bKey + } + // For string or array keys, convert to string for comparison + const aKeyStr = Array.isArray(aKey) ? aKey.join(',') : String(aKey) + const bKeyStr = Array.isArray(bKey) ? bKey.join(',') : String(bKey) + return aKeyStr < bKeyStr ? -1 : aKeyStr > bKeyStr ? 1 : 0 + }, + ) + .sort( + ( + [[_aKey, [_aValue, aIndex]], _aMultiplicity], + [[_bKey, [_bValue, bIndex]], _bMultiplicity], + ) => { + // lexically compare the index + return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0 + }, + ) +} + +/** + * Helper function to group sorted results by group key and extract values. + * Results should already be sorted by fractional index. + * Returns a Map of group key -> array of values (ordered by fractional index). + */ +function groupResultsByKey( + sortedResults: Array, + groupKeyFn: (key: any, value: any) => TGroupKey, +): Map> { + const groupedValues = new Map>() + for (const [key, [value, _index]] of sortedResults) { + const group = groupKeyFn(key, value) + const list = groupedValues.get(group) ?? [] + // Extract the numeric value from the value object + list.push((value as { value: number }).value) + groupedValues.set(group, list) + } + return groupedValues +} From 391067404c05ad3b77ae3ea4d77724922a003804 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 10 Dec 2025 13:35:56 +0100 Subject: [PATCH 4/7] Remove unnecessary changes --- packages/rxdb-db-collection/src/rxdb.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rxdb-db-collection/src/rxdb.ts b/packages/rxdb-db-collection/src/rxdb.ts index 23880c534..6ec59cfb6 100644 --- a/packages/rxdb-db-collection/src/rxdb.ts +++ b/packages/rxdb-db-collection/src/rxdb.ts @@ -280,6 +280,7 @@ export function rxdbCollectionOptions(config: RxDBCollectionConfig) { onUpdate: async (params) => { debug(`update`, params) const mutations = params.transaction.mutations.filter( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition (m) => m.type === `update`, ) @@ -296,6 +297,7 @@ export function rxdbCollectionOptions(config: RxDBCollectionConfig) { onDelete: async (params) => { debug(`delete`, params) const mutations = params.transaction.mutations.filter( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition (m) => m.type === `delete`, ) const ids = mutations.map((mutation) => getKey(mutation.original)) From 606581819f0692eb3de72297e095561e9d692f52 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 12:38:20 +0000 Subject: [PATCH 5/7] ci: apply automated fixes --- .../groupedOrderByWithFractionalIndex.test.ts | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts b/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts index 850d0c99a..a4734d4a5 100644 --- a/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts +++ b/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts @@ -21,8 +21,8 @@ describe(`Operators`, () => { >() const tracker = new MessageTracker< [[string, string], [{ id: string; value: number }, string]] - >() - + >() + const groupKeyFn = (key: [string, string]) => key[0] input.pipe( @@ -75,7 +75,10 @@ describe(`Operators`, () => { [string, [{ id: string; group: string; value: number }, string]] >() - const groupKeyFn = (_key: string, value: { id: string; group: string; value: number }) => value.group + const groupKeyFn = ( + _key: string, + value: { id: string; group: string; value: number }, + ) => value.group input.pipe( groupedOrderByWithFractionalIndex((item) => item.value, { @@ -152,7 +155,9 @@ describe(`Operators`, () => { // Initial should have 2 items (limit 2): values 1 and 3 const initialResult = tracker.getResult(compareFractionalIndex) expect(initialResult.sortedResults.length).toBe(2) - const sortedInitialResults = sortByKeyAndIndex(initialResult.sortedResults) + const sortedInitialResults = sortByKeyAndIndex( + initialResult.sortedResults, + ) const initialValues = sortedInitialResults.map( ([_key, [value, _index]]) => value.value, ) @@ -244,7 +249,10 @@ describe(`Operators`, () => { [string, [{ id: string; group: string; value: number }, string]] >() - const groupKeyFn = (_key: string, value: { id: string; group: string; value: number }) => value.group + const groupKeyFn = ( + _key: string, + value: { id: string; group: string; value: number }, + ) => value.group input.pipe( groupedOrderByWithFractionalIndex((item) => item.value, { @@ -274,8 +282,13 @@ describe(`Operators`, () => { expect(initialResult.sortedResults.length).toBe(4) // Sort all results by fractional index first, then group by group key - const sortedInitialResults = sortByKeyAndIndex(initialResult.sortedResults) - const initialGroupedValues = groupResultsByKey(sortedInitialResults, groupKeyFn) + const sortedInitialResults = sortByKeyAndIndex( + initialResult.sortedResults, + ) + const initialGroupedValues = groupResultsByKey( + sortedInitialResults, + groupKeyFn, + ) // group1 should have values 10, 20 (top 2 by ascending value, ordered by fractional index) expect(initialGroupedValues.get(`group1`)).toEqual([10, 20]) @@ -321,7 +334,8 @@ describe(`Operators`, () => { // Check that the fractional index of the added value (5) is smaller than the index of value 10 const finalResult = tracker.getResult(compareFractionalIndex) const value10Entry = finalResult.sortedResults.find( - ([_key, [value, _index]]) => value.value === 10 && value.group === `group1`, + ([_key, [value, _index]]) => + value.value === 10 && value.group === `group1`, ) expect(value10Entry).toBeDefined() const [_value10Key, [_value10Value, value10Idx]] = value10Entry! @@ -420,7 +434,8 @@ describe(`Operators`, () => { [string, [{ id: string; value: number }, string]] >() - const groupKeyFn = (key: string, _value: { id: string; value: number }) => key.split(`:`)[0]! + const groupKeyFn = (key: string, _value: { id: string; value: number }) => + key.split(`:`)[0]! input.pipe( groupedOrderByWithFractionalIndex((item) => item.value, { @@ -577,7 +592,9 @@ describe(`Operators`, () => { // Initial: values 1, 2 const initialResult = tracker.getResult(compareFractionalIndex) - const sortedInitialResults = sortByKeyAndIndex(initialResult.sortedResults) + const sortedInitialResults = sortByKeyAndIndex( + initialResult.sortedResults, + ) const initialValues = sortedInitialResults.map( ([_key, [value, _index]]) => value.value, ) From d17bf7b9f77a03b4932123ed305af9ace44c1b45 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 10 Dec 2025 13:48:33 +0100 Subject: [PATCH 6/7] Fix failing test --- .../groupedOrderByWithFractionalIndex.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts b/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts index a4734d4a5..f7df002b0 100644 --- a/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts +++ b/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts @@ -295,6 +295,13 @@ describe(`Operators`, () => { // group2 should have values 5, 15 (top 2 by ascending value, ordered by fractional index) expect(initialGroupedValues.get(`group2`)).toEqual([5, 15]) + // Capture the fractional index of value 10 before reset + const value10Entry = sortedInitialResults.find( + ([_key, [value, _index]]) => value.value === 10 && value.group === `group1`, + ) + expect(value10Entry).toBeDefined() + const [_value10Key, [_value10Value, value10Idx]] = value10Entry! + tracker.reset() // Update only group1 - add a better value @@ -332,13 +339,6 @@ describe(`Operators`, () => { expect(additionValue.id).toBe(`g1-c`) // Check that the fractional index of the added value (5) is smaller than the index of value 10 - const finalResult = tracker.getResult(compareFractionalIndex) - const value10Entry = finalResult.sortedResults.find( - ([_key, [value, _index]]) => - value.value === 10 && value.group === `group1`, - ) - expect(value10Entry).toBeDefined() - const [_value10Key, [_value10Value, value10Idx]] = value10Entry! expect(additionIdx < value10Idx).toBe(true) }) From e9305b8abb76fab919ec3cd52be4792a11d53e14 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 12:54:28 +0000 Subject: [PATCH 7/7] ci: apply automated fixes --- .../tests/operators/groupedOrderByWithFractionalIndex.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts b/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts index f7df002b0..d88617b88 100644 --- a/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts +++ b/packages/db-ivm/tests/operators/groupedOrderByWithFractionalIndex.test.ts @@ -297,7 +297,8 @@ describe(`Operators`, () => { // Capture the fractional index of value 10 before reset const value10Entry = sortedInitialResults.find( - ([_key, [value, _index]]) => value.value === 10 && value.group === `group1`, + ([_key, [value, _index]]) => + value.value === 10 && value.group === `group1`, ) expect(value10Entry).toBeDefined() const [_value10Key, [_value10Value, value10Idx]] = value10Entry!