@electric-sql/d2ts
Version:
D2TS is a TypeScript implementation of Differential Dataflow.
281 lines (234 loc) • 6.96 kB
text/typescript
import { describe, test, expect, beforeEach, afterEach } from 'vitest'
import { D2 } from '../../src/d2.js'
import { MultiSet } from '../../src/multiset.js'
import {
filterBy as inMemoryFilterBy,
output,
} from '../../src/operators/index.js'
import { filterBy as sqliteFilterBy } from '../../src/sqlite/operators/filterBy.js'
import { Message, MessageType } from '../../src/types.js'
import { KeyValue } from '../../src/types.js'
import { BetterSQLite3Wrapper } from '../../src/sqlite/database.js'
import Database from 'better-sqlite3'
describe('Operators', () => {
describe('FilterBy operation', () => {
testFilterBy(inMemoryFilterBy)
})
})
describe('SQLite Operators', () => {
describe('FilterBy operation', () => {
let db: BetterSQLite3Wrapper
beforeEach(() => {
const sqlite = new Database(':memory:')
db = new BetterSQLite3Wrapper(sqlite)
})
afterEach(() => {
db.close()
})
const wrappedFilterBy = ((stream) => {
return sqliteFilterBy(stream, db)
}) as typeof inMemoryFilterBy
testFilterBy(wrappedFilterBy)
})
})
function testFilterBy(filterBy: typeof inMemoryFilterBy) {
test('filterBy operator exists', () => {
expect(typeof filterBy).toBe('function')
})
test('filterBy basic test', () => {
const graph = new D2({ initialFrontier: 0 })
const inputA = graph.newInput<KeyValue<number, string>>()
const inputB = graph.newInput<KeyValue<number, boolean>>()
const messages: Message<any>[] = []
// Use the filterBy operator
inputA.pipe(
filterBy(inputB),
output((message) => {
messages.push(message)
}),
)
graph.finalize()
// Send data to the main stream
inputA.sendData(
1,
new MultiSet<KeyValue<number, string>>([
[[1, 'apple'], 1],
[[2, 'banana'], 1],
]),
)
inputA.sendFrontier(1)
// Send filter keys to the filter stream
inputB.sendData(
1,
new MultiSet<KeyValue<number, boolean>>([[[1, true], 1]]),
)
inputB.sendFrontier(1)
graph.run()
// Check if we got any frontier messages
const frontierMessages = messages.filter(
(m) => m.type === MessageType.FRONTIER,
)
expect(frontierMessages.length).toBeGreaterThan(0)
})
test('filterBy with empty filter stream', () => {
const graph = new D2({ initialFrontier: 0 })
const inputA = graph.newInput<KeyValue<number, string>>()
const inputB = graph.newInput<KeyValue<number, boolean>>()
const messages: Message<any>[] = []
inputA.pipe(
filterBy(inputB),
output((message) => {
messages.push(message)
}),
)
graph.finalize()
// Send data to the main stream
inputA.sendData(
1,
new MultiSet<KeyValue<number, string>>([
[[1, 'apple'], 1],
[[2, 'banana'], 1],
]),
)
inputA.sendFrontier(1)
// Send empty filter data
inputB.sendData(1, new MultiSet([]))
inputB.sendFrontier(1)
graph.run()
// No data messages should be returned since filter stream is empty
const dataMessages = messages.filter((m) => m.type === MessageType.DATA)
expect(dataMessages.length).toBe(0)
})
test('filterBy with late arriving filter data', () => {
const graph = new D2({ initialFrontier: 0 })
const inputA = graph.newInput<KeyValue<number, string>>()
const inputB = graph.newInput<KeyValue<number, boolean>>()
const messages: Message<any>[] = []
inputA.pipe(
filterBy(inputB),
output((message) => {
messages.push(message)
}),
)
graph.finalize()
// Send data to the main stream first
inputA.sendData(
1,
new MultiSet<KeyValue<number, string>>([
[[1, 'apple'], 1],
[[2, 'banana'], 1],
[[3, 'cherry'], 1],
]),
)
inputA.sendFrontier(1)
graph.run()
// No data messages yet because filter stream hasn't provided any keys
const initialDataMessages = messages.filter(
(m) => m.type === MessageType.DATA,
)
expect(initialDataMessages.length).toBe(0)
// Now send filter keys
inputB.sendData(
1,
new MultiSet<KeyValue<number, boolean>>([
[[2, true], 1],
[[3, false], 1],
]),
)
inputB.sendFrontier(1)
graph.run()
// Now we should have frontier messages
const frontierMessages = messages.filter(
(m) => m.type === MessageType.FRONTIER,
)
expect(frontierMessages.length).toBeGreaterThan(0)
})
test('filterBy with updates to filter stream', () => {
const graph = new D2({ initialFrontier: 0 })
const inputA = graph.newInput<KeyValue<number, string>>()
const inputB = graph.newInput<KeyValue<number, boolean>>()
const messages: Message<any>[] = []
inputA.pipe(
filterBy(inputB),
output((message) => {
messages.push(message)
}),
)
graph.finalize()
// Send initial data
inputA.sendData(
1,
new MultiSet<KeyValue<number, string>>([
[[1, 'apple'], 1],
[[2, 'banana'], 1],
[[3, 'cherry'], 1],
[[4, 'date'], 1],
]),
)
inputA.sendFrontier(1)
// Send initial filter keys
inputB.sendData(
1,
new MultiSet<KeyValue<number, boolean>>([
[[1, true], 1],
[[3, true], 1],
]),
)
inputB.sendFrontier(1)
graph.run()
// Now update the filter stream with new keys
inputB.sendData(
2,
new MultiSet<KeyValue<number, boolean>>([
[[2, true], 1],
[[4, true], 1],
]),
)
inputB.sendFrontier(2)
graph.run()
// We should have frontier messages
const frontierMessages = messages.filter(
(m) => m.type === MessageType.FRONTIER,
)
expect(frontierMessages.length).toBeGreaterThan(0)
})
test('filterBy with negative multiplicities', () => {
const graph = new D2({ initialFrontier: 0 })
const inputA = graph.newInput<KeyValue<number, string>>()
const inputB = graph.newInput<KeyValue<number, boolean>>()
const messages: Message<any>[] = []
inputA.pipe(
filterBy(inputB),
output((message) => {
messages.push(message)
}),
)
graph.finalize()
// Send data to the main stream with negative multiplicity
inputA.sendData(
1,
new MultiSet<KeyValue<number, string>>([
[[1, 'apple'], 1],
[[2, 'banana'], -1],
[[3, 'cherry'], 1],
]),
)
inputA.sendFrontier(1)
// Send filter keys
inputB.sendData(
1,
new MultiSet<KeyValue<number, boolean>>([
[[1, true], 1],
[[2, true], 1],
[[3, true], 1],
]),
)
inputB.sendFrontier(1)
graph.run()
// We should have frontier messages
const frontierMessages = messages.filter(
(m) => m.type === MessageType.FRONTIER,
)
expect(frontierMessages.length).toBeGreaterThan(0)
})
}