brainy/tests/unit/indexes/columnStore/segment-cursor.test.ts

271 lines
9.3 KiB
TypeScript
Raw Normal View History

/**
* @module segment-cursor.test
* @description Tests for the segment cursor: binary search, iteration, tombstones.
*/
import { describe, it, expect } from 'vitest'
import { ColumnSegmentCursor, TailBufferCursor } from '../../../../src/indexes/columnStore/ColumnSegmentCursor.js'
import { ValueType, CIDX_MAGIC, CIDX_VERSION } from '../../../../src/indexes/columnStore/types.js'
import { RoaringBitmap32 } from '../../../../src/utils/roaring/index.js'
function makeNumericCursor(values: number[], entityIds: number[], tombstones: number[] = []): ColumnSegmentCursor {
return new ColumnSegmentCursor(
{
magic: CIDX_MAGIC,
version: CIDX_VERSION,
fieldName: 'test',
fieldNameLength: 4,
valueType: ValueType.Number,
level: 0,
codec: 0,
flags: 0,
count: values.length
},
values,
new Uint32Array(entityIds),
new RoaringBitmap32(tombstones)
)
}
function makeStringCursor(values: string[], entityIds: number[], tombstones: number[] = []): ColumnSegmentCursor {
return new ColumnSegmentCursor(
{
magic: CIDX_MAGIC,
version: CIDX_VERSION,
fieldName: 'status',
fieldNameLength: 6,
valueType: ValueType.String,
level: 0,
codec: 0,
flags: 0,
count: values.length
},
values,
new Uint32Array(entityIds),
new RoaringBitmap32(tombstones)
)
}
describe('ColumnSegmentCursor', () => {
// -----------------------------------------------------------------------
// Binary search
// -----------------------------------------------------------------------
describe('binarySearchValue (numeric)', () => {
it('finds exact match', () => {
const c = makeNumericCursor([100, 200, 300, 400, 500], [1, 2, 3, 4, 5])
const r = c.binarySearchValue(300)
expect(r.found).toBe(true)
expect(r.index).toBe(2)
})
it('returns insertion point when not found', () => {
const c = makeNumericCursor([100, 200, 400, 500], [1, 2, 3, 4])
const r = c.binarySearchValue(300)
expect(r.found).toBe(false)
expect(r.index).toBe(2) // would insert at position 2
})
it('handles before-first', () => {
const c = makeNumericCursor([100, 200, 300], [1, 2, 3])
const r = c.binarySearchValue(50)
expect(r.found).toBe(false)
expect(r.index).toBe(0)
})
it('handles after-last', () => {
const c = makeNumericCursor([100, 200, 300], [1, 2, 3])
const r = c.binarySearchValue(999)
expect(r.found).toBe(false)
expect(r.index).toBe(3)
})
it('finds first occurrence of duplicates', () => {
const c = makeNumericCursor([100, 200, 200, 200, 300], [1, 2, 3, 4, 5])
const r = c.binarySearchValue(200)
expect(r.found).toBe(true)
expect(r.index).toBe(1) // first occurrence
})
})
describe('binarySearchValue (string)', () => {
it('finds exact match', () => {
const c = makeStringCursor(['active', 'inactive', 'pending'], [1, 2, 3])
const r = c.binarySearchValue('inactive')
expect(r.found).toBe(true)
expect(r.index).toBe(1)
})
it('returns insertion point when not found', () => {
const c = makeStringCursor(['active', 'pending'], [1, 2])
const r = c.binarySearchValue('inactive')
expect(r.found).toBe(false)
expect(r.index).toBe(1)
})
})
// -----------------------------------------------------------------------
// Range search
// -----------------------------------------------------------------------
describe('binarySearchRange', () => {
it('finds range with inclusive bounds', () => {
const c = makeNumericCursor([100, 200, 300, 400, 500], [1, 2, 3, 4, 5])
const { startIdx, endIdx } = c.binarySearchRange(200, 400)
expect(startIdx).toBe(1) // 200
expect(endIdx).toBe(4) // excludes 500
})
it('handles range beyond data', () => {
const c = makeNumericCursor([100, 200, 300], [1, 2, 3])
const { startIdx, endIdx } = c.binarySearchRange(0, 999)
expect(startIdx).toBe(0)
expect(endIdx).toBe(3) // all entries
})
it('handles empty range', () => {
const c = makeNumericCursor([100, 200, 300], [1, 2, 3])
const { startIdx, endIdx } = c.binarySearchRange(150, 180)
expect(startIdx).toBe(1) // insertion point for 150
expect(endIdx).toBe(1) // nothing between 150 and 180
})
})
// -----------------------------------------------------------------------
// Point and range queries
// -----------------------------------------------------------------------
describe('getEntityIdsForValue', () => {
it('returns matching entity IDs', () => {
const c = makeNumericCursor([100, 200, 200, 300], [10, 20, 30, 40])
expect(c.getEntityIdsForValue(200)).toEqual([20, 30])
})
it('returns empty for no match', () => {
const c = makeNumericCursor([100, 200, 300], [10, 20, 30])
expect(c.getEntityIdsForValue(999)).toEqual([])
})
it('excludes tombstoned entries', () => {
const c = makeNumericCursor([100, 200, 200, 300], [10, 20, 30, 40], [1]) // position 1 tombstoned
expect(c.getEntityIdsForValue(200)).toEqual([30]) // position 1 (entityId=20) excluded
})
})
describe('getEntityIdsInRange', () => {
it('returns entities in range', () => {
const c = makeNumericCursor([100, 200, 300, 400, 500], [1, 2, 3, 4, 5])
expect(c.getEntityIdsInRange(200, 400)).toEqual([2, 3, 4])
})
it('excludes tombstoned', () => {
const c = makeNumericCursor([100, 200, 300, 400, 500], [1, 2, 3, 4, 5], [2]) // position 2 (value=300)
expect(c.getEntityIdsInRange(200, 400)).toEqual([2, 4])
})
})
// -----------------------------------------------------------------------
// Iteration
// -----------------------------------------------------------------------
describe('iterateForward', () => {
it('yields all entries in order', () => {
const c = makeNumericCursor([100, 200, 300], [1, 2, 3])
const entries = [...c.iterateForward()]
expect(entries).toHaveLength(3)
expect(entries[0]).toEqual({ value: 100, entityIntId: 1, position: 0 })
expect(entries[2]).toEqual({ value: 300, entityIntId: 3, position: 2 })
})
it('skips tombstoned entries', () => {
const c = makeNumericCursor([100, 200, 300], [1, 2, 3], [1])
const entries = [...c.iterateForward()]
expect(entries).toHaveLength(2)
expect(entries.map(e => e.entityIntId)).toEqual([1, 3])
})
it('starts from offset', () => {
const c = makeNumericCursor([100, 200, 300, 400], [1, 2, 3, 4])
const entries = [...c.iterateForward(2)]
expect(entries).toHaveLength(2)
expect(entries[0].value).toBe(300)
})
})
describe('iterateBackward', () => {
it('yields all entries in reverse order', () => {
const c = makeNumericCursor([100, 200, 300], [1, 2, 3])
const entries = [...c.iterateBackward()]
expect(entries).toHaveLength(3)
expect(entries[0]).toEqual({ value: 300, entityIntId: 3, position: 2 })
expect(entries[2]).toEqual({ value: 100, entityIntId: 1, position: 0 })
})
it('skips tombstoned entries', () => {
const c = makeNumericCursor([100, 200, 300], [1, 2, 3], [1])
const entries = [...c.iterateBackward()]
expect(entries).toHaveLength(2)
expect(entries.map(e => e.entityIntId)).toEqual([3, 1])
})
})
// -----------------------------------------------------------------------
// Zone map (min/max)
// -----------------------------------------------------------------------
describe('zone map', () => {
it('minValue returns first value', () => {
const c = makeNumericCursor([100, 200, 300], [1, 2, 3])
expect(c.minValue).toBe(100)
})
it('maxValue returns last value', () => {
const c = makeNumericCursor([100, 200, 300], [1, 2, 3])
expect(c.maxValue).toBe(300)
})
it('handles empty segment', () => {
const c = makeNumericCursor([], [])
expect(c.minValue).toBeUndefined()
expect(c.maxValue).toBeUndefined()
})
})
describe('liveCount', () => {
it('subtracts tombstones from count', () => {
const c = makeNumericCursor([100, 200, 300], [1, 2, 3], [0, 2])
expect(c.liveCount).toBe(1)
})
})
// -----------------------------------------------------------------------
// TailBufferCursor
// -----------------------------------------------------------------------
describe('TailBufferCursor', () => {
it('iterates forward', () => {
const c = new TailBufferCursor([10, 20, 30], new Uint32Array([1, 2, 3]), ValueType.Number)
const entries = [...c.iterateForward()]
expect(entries).toHaveLength(3)
expect(entries[0].value).toBe(10)
})
it('iterates backward', () => {
const c = new TailBufferCursor([10, 20, 30], new Uint32Array([1, 2, 3]), ValueType.Number)
const entries = [...c.iterateBackward()]
expect(entries[0].value).toBe(30)
})
it('point query returns matching IDs', () => {
const c = new TailBufferCursor([10, 20, 20, 30], new Uint32Array([1, 2, 3, 4]), ValueType.Number)
expect(c.getEntityIdsForValue(20)).toEqual([2, 3])
})
it('min/max values', () => {
const c = new TailBufferCursor([10, 20, 30], new Uint32Array([1, 2, 3]), ValueType.Number)
expect(c.minValue).toBe(10)
expect(c.maxValue).toBe(30)
})
})
})