UNPKG

@electric-sql/d2mini

Version:

D2Mini is a minimal implementation of Differential Dataflow for performing in-memory incremental view maintenance.

478 lines (422 loc) 14 kB
import { describe, test, expect, beforeEach, afterEach } from 'vitest' import { D2 } from '../../src/d2.js' import { MultiSet } from '../../src/multiset.js' import { output } from '../../src/operators/index.js' import { topKWithIndex } from '../../src/operators/topK.js' describe('Operators', () => { describe('TopKWithIndex operation', () => { test('initial results with limit - no key', () => { const graph = new D2() const input = graph.newInput< [ null, { id: number value: string }, ] >() let latestMessage: any = null input.pipe( topKWithIndex((a, b) => a.value.localeCompare(b.value), { limit: 3 }), output((message) => { latestMessage = message }), ) graph.finalize() input.sendData( new MultiSet([ [[null, { id: 1, value: 'a' }], 1], [[null, { id: 2, value: 'z' }], 1], [[null, { id: 3, value: 'b' }], 1], [[null, { id: 4, value: 'y' }], 1], [[null, { id: 5, value: 'c' }], 1], ]), ) graph.run() expect(latestMessage).not.toBeNull() const result = latestMessage.getInner() const sortedResult = sortByIndexAndId(result) expect(sortedResult).toEqual([ [[null, [{ id: 1, value: 'a' }, 0]], 1], [[null, [{ id: 3, value: 'b' }, 1]], 1], [[null, [{ id: 5, value: 'c' }, 2]], 1], ]) }) test('initial results with limit and offset - no key', () => { const graph = new D2() const input = graph.newInput< [ null, { id: number value: string }, ] >() let latestMessage: any = null input.pipe( topKWithIndex((a, b) => a.value.localeCompare(b.value), { limit: 3, offset: 2, }), output((message) => { latestMessage = message }), ) graph.finalize() input.sendData( new MultiSet([ [[null, { id: 1, value: 'a' }], 1], [[null, { id: 2, value: 'z' }], 1], [[null, { id: 3, value: 'b' }], 1], [[null, { id: 4, value: 'y' }], 1], [[null, { id: 5, value: 'c' }], 1], ]), ) graph.run() expect(latestMessage).not.toBeNull() const result = latestMessage.getInner() const sortedResult = sortByIndexAndId(result) expect(sortedResult).toEqual([ [[null, [{ id: 5, value: 'c' }, 2]], 1], [[null, [{ id: 4, value: 'y' }, 3]], 1], [[null, [{ id: 2, value: 'z' }, 4]], 1], ]) }) test('initial results with limit - with key', () => { const graph = new D2() const input = graph.newInput< [ string, { id: number value: string }, ] >() let latestMessage: any = null input.pipe( topKWithIndex((a, b) => a.value.localeCompare(b.value), { limit: 3 }), output((message) => { latestMessage = message }), ) graph.finalize() input.sendData( new MultiSet([ [['one', { id: 1, value: '9' }], 1], [['one', { id: 2, value: '8' }], 1], [['one', { id: 3, value: '7' }], 1], [['one', { id: 4, value: '6' }], 1], [['one', { id: 5, value: '5' }], 1], [['two', { id: 6, value: '4' }], 1], [['two', { id: 7, value: '3' }], 1], [['two', { id: 8, value: '2' }], 1], [['two', { id: 9, value: '1' }], 1], [['two', { id: 10, value: '0' }], 1], ]), ) graph.run() expect(latestMessage).not.toBeNull() const result = latestMessage.getInner() const sortedResult = sortByKeyIndexAndId(result) expect(sortedResult).toEqual([ [['one', [{ id: 5, value: '5' }, 0]], 1], [['one', [{ id: 4, value: '6' }, 1]], 1], [['one', [{ id: 3, value: '7' }, 2]], 1], [['two', [{ id: 10, value: '0' }, 0]], 1], [['two', [{ id: 9, value: '1' }, 1]], 1], [['two', [{ id: 8, value: '2' }, 2]], 1], ]) }) test('incremental update - removing a row', () => { const graph = new D2() const input = graph.newInput< [ null, { id: number value: string }, ] >() let latestMessage: any = null input.pipe( topKWithIndex((a, b) => a.value.localeCompare(b.value), { limit: 3 }), output((message) => { latestMessage = message }), ) graph.finalize() // Initial data input.sendData( new MultiSet([ [[null, { id: 1, value: 'a' }], 1], [[null, { id: 2, value: 'b' }], 1], [[null, { id: 3, value: 'c' }], 1], [[null, { id: 4, value: 'd' }], 1], ]), ) graph.run() // Initial result should be first three items with indices let result = latestMessage.getInner() let sortedResult = sortByIndexAndId(result) expect(sortedResult).toEqual([ [[null, [{ id: 1, value: 'a' }, 0]], 1], [[null, [{ id: 2, value: 'b' }, 1]], 1], [[null, [{ id: 3, value: 'c' }, 2]], 1], ]) // Remove 'b' from the result set input.sendData(new MultiSet([[[null, { id: 2, value: 'b' }], -1]])) graph.run() // Result should show 'b' being removed with its old index, // 'c' moving from index 2 to 1, and 'd' being added at index 2 result = latestMessage.getInner() sortedResult = sortByMultiplicityIndexAndId(result) expect(sortedResult).toEqual([ [[null, [{ id: 2, value: 'b' }, 1]], -1], // Removed row with its old index [[null, [{ id: 3, value: 'c' }, 2]], -1], // 'c' removed from old index 2 [[null, [{ id: 3, value: 'c' }, 1]], 1], // 'c' moved from index 2 to 1 [[null, [{ id: 4, value: 'd' }, 2]], 1], // New row added at index 2 ]) }) test('incremental update - adding rows that push existing rows out of limit window', () => { const graph = new D2() const input = graph.newInput< [ null, { id: number value: string }, ] >() let latestMessage: any = null input.pipe( topKWithIndex((a, b) => a.value.localeCompare(b.value), { limit: 3 }), output((message) => { latestMessage = message }), ) graph.finalize() // Initial data input.sendData( new MultiSet([ [[null, { id: 1, value: 'c' }], 1], [[null, { id: 2, value: 'd' }], 1], [[null, { id: 3, value: 'e' }], 1], ]), ) graph.run() // Initial result should be all three items with indices let result = latestMessage.getInner() let sortedResult = sortByIndexAndId(result) expect(sortedResult).toEqual([ [[null, [{ id: 1, value: 'c' }, 0]], 1], [[null, [{ id: 2, value: 'd' }, 1]], 1], [[null, [{ id: 3, value: 'e' }, 2]], 1], ]) // Add two new rows that should appear before existing rows input.sendData( new MultiSet([ [[null, { id: 4, value: 'a' }], 1], [[null, { id: 5, value: 'b' }], 1], ]), ) graph.run() // Result should show: // - 'a' and 'b' being added at indices 0 and 1 // - 'c' moving from index 0 to 2 // - 'd' and 'e' being removed as they're pushed out of the limit window result = latestMessage.getInner() sortedResult = sortByMultiplicityIndexAndId(result) expect(sortedResult).toEqual([ [[null, [{ id: 1, value: 'c' }, 0]], -1], // 'c' removed from old index 0 [[null, [{ id: 2, value: 'd' }, 1]], -1], // 'd' removed from index 1 [[null, [{ id: 3, value: 'e' }, 2]], -1], // 'e' removed from index 2 [[null, [{ id: 4, value: 'a' }, 0]], 1], // New row at index 0 [[null, [{ id: 5, value: 'b' }, 1]], 1], // New row at index 1 [[null, [{ id: 1, value: 'c' }, 2]], 1], // 'c' added at new index 2 ]) }) test('incremental update - changing a value that affects ordering', () => { const graph = new D2() const input = graph.newInput< [ null, { id: number value: string }, ] >() let latestMessage: any = null input.pipe( topKWithIndex((a, b) => a.value.localeCompare(b.value), { limit: 3 }), output((message) => { latestMessage = message }), ) graph.finalize() // Initial data input.sendData( new MultiSet([ [[null, { id: 1, value: 'a' }], 1], [[null, { id: 2, value: 'b' }], 1], [[null, { id: 3, value: 'c' }], 1], ]), ) graph.run() // Initial result should be all three items with indices let result = latestMessage.getInner() let sortedResult = sortByIndexAndId(result) expect(sortedResult).toEqual([ [[null, [{ id: 1, value: 'a' }, 0]], 1], [[null, [{ id: 2, value: 'b' }, 1]], 1], [[null, [{ id: 3, value: 'c' }, 2]], 1], ]) // Change 'a' to 'z' which should move it to the end, outside the limit input.sendData( new MultiSet([ [[null, { id: 1, value: 'a' }], -1], [[null, { id: 1, value: 'z' }], 1], ]), ) graph.run() // Result should show: // - 'a' being removed from index 0 // - 'b' moving from index 1 to 0 // - 'c' moving from index 2 to 1 // - 'z' being added at index 2 result = latestMessage.getInner() sortedResult = sortByMultiplicityIndexAndId(result) expect(sortedResult).toEqual([ [[null, [{ id: 1, value: 'a' }, 0]], -1], // 'a' removed from index 0 [[null, [{ id: 2, value: 'b' }, 1]], -1], // 'b' removed from old index 1 [[null, [{ id: 3, value: 'c' }, 2]], -1], // 'c' removed from old index 2 [[null, [{ id: 2, value: 'b' }, 0]], 1], // 'b' added at new index 0 [[null, [{ id: 3, value: 'c' }, 1]], 1], // 'c' added at new index 1 [[null, [{ id: 1, value: 'z' }, 2]], 1], // 'z' added at index 2 ]) }) test('incremental update with offset - items moving in and out of window', () => { const graph = new D2() const input = graph.newInput< [ null, { id: number value: string }, ] >() let latestMessage: any = null input.pipe( topKWithIndex((a, b) => a.value.localeCompare(b.value), { limit: 2, offset: 1, }), output((message) => { latestMessage = message }), ) graph.finalize() // Initial data - a, b, c, d, e input.sendData( new MultiSet([ [[null, { id: 1, value: 'a' }], 1], [[null, { id: 2, value: 'b' }], 1], [[null, { id: 3, value: 'c' }], 1], [[null, { id: 4, value: 'd' }], 1], [[null, { id: 5, value: 'e' }], 1], ]), ) graph.run() // Initial result should be b, c (offset 1, limit 2) let result = latestMessage.getInner() let sortedResult = sortByIndexAndId(result) expect(sortedResult).toEqual([ [[null, [{ id: 2, value: 'b' }, 1]], 1], [[null, [{ id: 3, value: 'c' }, 2]], 1], ]) // Add a new item 'aa' that should be between 'a' and 'b' input.sendData(new MultiSet([[[null, { id: 6, value: 'aa' }], 1]])) graph.run() // Result should show: // - 'aa' being added at index 1 // - 'b' moving from index 1 to 2 // - 'c' being removed as it's pushed out of the window result = latestMessage.getInner() sortedResult = sortByMultiplicityIndexAndId(result) expect(sortedResult).toEqual([ [[null, [{ id: 2, value: 'b' }, 1]], -1], // 'b' removed from old index 1 [[null, [{ id: 3, value: 'c' }, 2]], -1], // 'c' removed from index 2 [[null, [{ id: 6, value: 'aa' }, 1]], 1], // 'aa' added at index 1 [[null, [{ id: 2, value: 'b' }, 2]], 1], // 'b' added at new index 2 ]) }) }) }) /** * Helper function to sort results by index and then id */ function sortByIndexAndId(results: any[]) { return [...results].sort( ( [[_aKey, [aValue, aIndex]], _aMultiplicity], [[_bKey, [bValue, bIndex]], _bMultiplicity], ) => { // First sort by index if (aIndex !== bIndex) { return aIndex - bIndex } // Then by id if indices are the same return aValue.id - bValue.id }, ) } /** * Helper function to sort results by key, then index, then id */ function sortByKeyIndexAndId(results: any[]) { return [...results].sort( ( [[aKey, [aValue, aIndex]], _aMultiplicity], [[bKey, [bValue, bIndex]], _bMultiplicity], ) => { // First sort by key if (aKey !== bKey) { return aKey < bKey ? -1 : 1 } // Then by index if (aIndex !== bIndex) { return aIndex - bIndex } // Then by id if indices are the same return aValue.id - bValue.id }, ) } /** * Helper function to sort results by multiplicity, then index, then id */ function sortByMultiplicityIndexAndId(results: any[]) { return [...results].sort( ( [[_aKey, [aValue, aIndex]], aMultiplicity], [[_bKey, [bValue, bIndex]], bMultiplicity], ) => { // First sort by multiplicity if (aMultiplicity !== bMultiplicity) { return aMultiplicity - bMultiplicity } // Then by index if (aIndex !== bIndex) { return aIndex - bIndex } // Then by id if indices are the same return aValue.id - bValue.id }, ) }