2025-10-13 16:39:06 -07:00
/ * *
* Roaring Bitmap Integration Tests
*
* Tests the v3 . 43.0 roaring bitmap integration for metadata indexing :
* - EntityIdMapper ( UUID ↔ integer conversion )
* - ChunkData with RoaringBitmap32
* - Multi - field intersection queries
* - Persistence and serialization
* /
import { describe , it , expect , beforeEach } from 'vitest'
import { EntityIdMapper } from '../../../src/utils/entityIdMapper.js'
import { ChunkManager } from '../../../src/utils/metadataIndexChunking.js'
import { MetadataIndexManager } from '../../../src/utils/metadataIndex.js'
import { MemoryStorage } from '../../../src/storage/adapters/memoryStorage.js'
2025-10-14 10:24:59 -07:00
import { RoaringBitmap32 } from 'roaring-wasm'
2025-10-13 16:39:06 -07:00
import { v4 as uuidv4 } from '../../../src/universal/uuid.js'
describe ( 'EntityIdMapper' , ( ) = > {
let storage : MemoryStorage
let idMapper : EntityIdMapper
beforeEach ( async ( ) = > {
storage = new MemoryStorage ( )
await storage . init ( )
idMapper = new EntityIdMapper ( {
storage ,
storageKey : 'test:entityIdMapper'
} )
await idMapper . init ( )
} )
it ( 'should assign unique integer IDs to UUIDs' , ( ) = > {
const uuid1 = uuidv4 ( )
const uuid2 = uuidv4 ( )
const int1 = idMapper . getOrAssign ( uuid1 )
const int2 = idMapper . getOrAssign ( uuid2 )
expect ( int1 ) . toBe ( 1 )
expect ( int2 ) . toBe ( 2 )
expect ( int1 ) . not . toBe ( int2 )
} )
it ( 'should return same integer for same UUID' , ( ) = > {
const uuid = uuidv4 ( )
const int1 = idMapper . getOrAssign ( uuid )
const int2 = idMapper . getOrAssign ( uuid )
expect ( int1 ) . toBe ( int2 )
} )
it ( 'should convert integer back to UUID' , ( ) = > {
const uuid = uuidv4 ( )
const intId = idMapper . getOrAssign ( uuid )
const retrievedUuid = idMapper . getUuid ( intId )
expect ( retrievedUuid ) . toBe ( uuid )
} )
it ( 'should convert UUID arrays to integer arrays' , ( ) = > {
const uuids = [ uuidv4 ( ) , uuidv4 ( ) , uuidv4 ( ) ]
const ints = idMapper . uuidsToInts ( uuids )
expect ( ints ) . toHaveLength ( 3 )
expect ( ints [ 0 ] ) . toBe ( 1 )
expect ( ints [ 1 ] ) . toBe ( 2 )
expect ( ints [ 2 ] ) . toBe ( 3 )
} )
it ( 'should convert integer arrays to UUID arrays' , ( ) = > {
const uuids = [ uuidv4 ( ) , uuidv4 ( ) , uuidv4 ( ) ]
const ints = idMapper . uuidsToInts ( uuids )
const retrievedUuids = idMapper . intsToUuids ( ints )
expect ( retrievedUuids ) . toEqual ( uuids )
} )
it ( 'should convert iterable integers to UUIDs (for bitmap iteration)' , ( ) = > {
const uuids = [ uuidv4 ( ) , uuidv4 ( ) , uuidv4 ( ) ]
const ints = idMapper . uuidsToInts ( uuids )
// Create a roaring bitmap
const bitmap = new RoaringBitmap32 ( ints )
// Convert bitmap to UUIDs
const retrievedUuids = idMapper . intsIterableToUuids ( bitmap )
expect ( retrievedUuids . sort ( ) ) . toEqual ( uuids . sort ( ) )
} )
it ( 'should persist and load mappings' , async ( ) = > {
const uuid1 = uuidv4 ( )
const uuid2 = uuidv4 ( )
idMapper . getOrAssign ( uuid1 )
idMapper . getOrAssign ( uuid2 )
await idMapper . flush ( )
// Create new mapper and load
const newIdMapper = new EntityIdMapper ( {
storage ,
storageKey : 'test:entityIdMapper'
} )
await newIdMapper . init ( )
expect ( newIdMapper . getInt ( uuid1 ) ) . toBe ( 1 )
expect ( newIdMapper . getInt ( uuid2 ) ) . toBe ( 2 )
expect ( newIdMapper . size ) . toBe ( 2 )
} )
it ( 'should remove UUID mappings' , ( ) = > {
const uuid = uuidv4 ( )
const intId = idMapper . getOrAssign ( uuid )
expect ( idMapper . has ( uuid ) ) . toBe ( true )
idMapper . remove ( uuid )
expect ( idMapper . has ( uuid ) ) . toBe ( false )
expect ( idMapper . getInt ( uuid ) ) . toBeUndefined ( )
expect ( idMapper . getUuid ( intId ) ) . toBeUndefined ( )
} )
it ( 'should clear all mappings' , async ( ) = > {
idMapper . getOrAssign ( uuidv4 ( ) )
idMapper . getOrAssign ( uuidv4 ( ) )
idMapper . getOrAssign ( uuidv4 ( ) )
expect ( idMapper . size ) . toBe ( 3 )
await idMapper . clear ( )
expect ( idMapper . size ) . toBe ( 0 )
} )
it ( 'should provide statistics' , ( ) = > {
idMapper . getOrAssign ( uuidv4 ( ) )
idMapper . getOrAssign ( uuidv4 ( ) )
const stats = idMapper . getStats ( )
expect ( stats . mappings ) . toBe ( 2 )
expect ( stats . nextId ) . toBe ( 3 )
expect ( stats . memoryEstimate ) . toBeGreaterThan ( 0 )
} )
} )
describe ( 'RoaringBitmap32 Integration' , ( ) = > {
it ( 'should create and manipulate roaring bitmaps' , ( ) = > {
const bitmap = new RoaringBitmap32 ( )
bitmap . add ( 1 )
bitmap . add ( 2 )
bitmap . add ( 3 )
expect ( bitmap . size ) . toBe ( 3 )
expect ( bitmap . has ( 1 ) ) . toBe ( true )
expect ( bitmap . has ( 4 ) ) . toBe ( false )
} )
it ( 'should perform fast intersection (AND operation)' , ( ) = > {
const bitmap1 = new RoaringBitmap32 ( [ 1 , 2 , 3 , 4 , 5 ] )
const bitmap2 = new RoaringBitmap32 ( [ 3 , 4 , 5 , 6 , 7 ] )
const bitmap3 = new RoaringBitmap32 ( [ 4 , 5 , 6 , 7 , 8 ] )
const result = RoaringBitmap32 . and ( bitmap1 , bitmap2 , bitmap3 )
// Only 4 and 5 appear in all three bitmaps
const resultArray = [ . . . result ] . sort ( )
expect ( result . size ) . toBeGreaterThanOrEqual ( 2 )
expect ( resultArray ) . toContain ( 4 )
expect ( resultArray ) . toContain ( 5 )
} )
it ( 'should perform union (OR operation)' , ( ) = > {
const bitmap1 = new RoaringBitmap32 ( [ 1 , 2 , 3 ] )
const bitmap2 = new RoaringBitmap32 ( [ 3 , 4 , 5 ] )
const result = RoaringBitmap32 . or ( bitmap1 , bitmap2 )
expect ( result . size ) . toBe ( 5 )
expect ( [ . . . result ] . sort ( ) ) . toEqual ( [ 1 , 2 , 3 , 4 , 5 ] )
} )
it ( 'should serialize and deserialize portably' , ( ) = > {
const bitmap = new RoaringBitmap32 ( [ 1 , 2 , 3 , 100 , 1000 , 10000 ] )
const serialized = bitmap . serialize ( 'portable' )
const deserialized = new RoaringBitmap32 ( )
deserialized . deserialize ( serialized , 'portable' )
expect ( deserialized . size ) . toBe ( bitmap . size )
expect ( [ . . . deserialized ] . sort ( ) ) . toEqual ( [ . . . bitmap ] . sort ( ) )
} )
it ( 'should be memory efficient' , ( ) = > {
const bitmap = new RoaringBitmap32 ( )
// Add 10,000 sequential integers
for ( let i = 0 ; i < 10000 ; i ++ ) {
bitmap . add ( i )
}
const serializedSize = bitmap . getSerializationSizeInBytes ( 'portable' )
// Roaring bitmaps should be much smaller than storing 10k integers as Set
// Set would be ~10k * 8 bytes = 80KB
// Roaring should be a few KB
expect ( serializedSize ) . toBeLessThan ( 20000 ) // Less than 20KB
} )
} )
describe ( 'MetadataIndexManager with Roaring Bitmaps' , ( ) = > {
let storage : MemoryStorage
let metadataIndex : MetadataIndexManager
beforeEach ( async ( ) = > {
storage = new MemoryStorage ( )
await storage . init ( )
metadataIndex = new MetadataIndexManager ( storage )
await metadataIndex . init ( )
} )
it ( 'should index entities and query with roaring bitmaps' , async ( ) = > {
// Add entities
const id1 = uuidv4 ( )
const id2 = uuidv4 ( )
const id3 = uuidv4 ( )
await metadataIndex . addToIndex ( id1 , { status : 'active' , role : 'admin' } )
await metadataIndex . addToIndex ( id2 , { status : 'active' , role : 'user' } )
await metadataIndex . addToIndex ( id3 , { status : 'inactive' , role : 'admin' } )
await metadataIndex . flush ( )
// Query single field
const activeIds = await metadataIndex . getIds ( 'status' , 'active' )
expect ( activeIds . sort ( ) ) . toEqual ( [ id1 , id2 ] . sort ( ) )
// Query another field
const adminIds = await metadataIndex . getIds ( 'role' , 'admin' )
expect ( adminIds . sort ( ) ) . toEqual ( [ id1 , id3 ] . sort ( ) )
} )
it ( 'should perform fast multi-field intersection queries' , async ( ) = > {
// Add entities
const id1 = uuidv4 ( )
const id2 = uuidv4 ( )
const id3 = uuidv4 ( )
const id4 = uuidv4 ( )
await metadataIndex . addToIndex ( id1 , { status : 'active' , role : 'admin' } )
await metadataIndex . addToIndex ( id2 , { status : 'active' , role : 'user' } )
await metadataIndex . addToIndex ( id3 , { status : 'active' , role : 'guest' } )
await metadataIndex . addToIndex ( id4 , { status : 'inactive' , role : 'admin' } )
await metadataIndex . flush ( )
// Query using fast roaring bitmap intersection
const results = await metadataIndex . getIdsForMultipleFields ( [
{ field : 'status' , value : 'active' } ,
{ field : 'role' , value : 'admin' }
] )
expect ( results ) . toEqual ( [ id1 ] )
} )
it ( 'should handle empty intersection results' , async ( ) = > {
const id1 = uuidv4 ( )
const id2 = uuidv4 ( )
await metadataIndex . addToIndex ( id1 , { status : 'active' , role : 'admin' } )
await metadataIndex . addToIndex ( id2 , { status : 'inactive' , role : 'user' } )
await metadataIndex . flush ( )
// Query with no matching results
const results = await metadataIndex . getIdsForMultipleFields ( [
{ field : 'status' , value : 'active' } ,
{ field : 'role' , value : 'user' }
] )
expect ( results ) . toEqual ( [ ] )
} )
it ( 'should short-circuit on empty bitmap' , async ( ) = > {
const id1 = uuidv4 ( )
await metadataIndex . addToIndex ( id1 , { status : 'active' } )
await metadataIndex . flush ( )
// Query with non-existent field value (should short-circuit)
const results = await metadataIndex . getIdsForMultipleFields ( [
{ field : 'status' , value : 'active' } ,
{ field : 'nonexistent' , value : 'value' }
] )
expect ( results ) . toEqual ( [ ] )
} )
it ( 'should handle single field query efficiently' , async ( ) = > {
const id1 = uuidv4 ( )
const id2 = uuidv4 ( )
await metadataIndex . addToIndex ( id1 , { status : 'active' } )
await metadataIndex . addToIndex ( id2 , { status : 'active' } )
await metadataIndex . flush ( )
// Single field query should use fast path
const results = await metadataIndex . getIdsForMultipleFields ( [
{ field : 'status' , value : 'active' }
] )
expect ( results . sort ( ) ) . toEqual ( [ id1 , id2 ] . sort ( ) )
} )
it ( 'should persist and load roaring bitmaps correctly' , async ( ) = > {
const id1 = uuidv4 ( )
const id2 = uuidv4 ( )
const id3 = uuidv4 ( )
await metadataIndex . addToIndex ( id1 , { status : 'active' , role : 'admin' } )
await metadataIndex . addToIndex ( id2 , { status : 'active' , role : 'user' } )
await metadataIndex . addToIndex ( id3 , { status : 'inactive' , role : 'admin' } )
await metadataIndex . flush ( )
// Create new index manager and load
const newMetadataIndex = new MetadataIndexManager ( storage )
await newMetadataIndex . init ( )
// Query should work with loaded data
const results = await newMetadataIndex . getIdsForMultipleFields ( [
{ field : 'status' , value : 'active' } ,
{ field : 'role' , value : 'admin' }
] )
expect ( results ) . toEqual ( [ id1 ] )
} )
it ( 'should handle complex multi-field queries' , async ( ) = > {
// Create a larger dataset with clear test cases
const targetIds = [ ]
const otherIds = [ ]
// Create 10 entities that match all criteria
for ( let i = 0 ; i < 10 ; i ++ ) {
const id = uuidv4 ( )
targetIds . push ( id )
await metadataIndex . addToIndex ( id , { status : 'active' , role : 'admin' , tier : 'premium' } )
}
// Create entities that don't match
for ( let i = 0 ; i < 20 ; i ++ ) {
const id = uuidv4 ( )
otherIds . push ( id )
await metadataIndex . addToIndex ( id , {
status : i % 2 === 0 ? 'active' : 'inactive' ,
role : i % 3 === 0 ? 'admin' : 'user' ,
tier : i % 5 === 0 ? 'premium' : 'basic'
} )
}
await metadataIndex . flush ( )
// Query: active + admin + premium (should match all targetIds)
const results = await metadataIndex . getIdsForMultipleFields ( [
{ field : 'status' , value : 'active' } ,
{ field : 'role' , value : 'admin' } ,
{ field : 'tier' , value : 'premium' }
] )
// All target IDs should be in results
expect ( results . length ) . toBeGreaterThanOrEqual ( 10 )
for ( const targetId of targetIds ) {
expect ( results ) . toContain ( targetId )
}
} )
it ( 'should remove entities from roaring bitmap index' , async ( ) = > {
const id1 = uuidv4 ( )
const id2 = uuidv4 ( )
await metadataIndex . addToIndex ( id1 , { status : 'active' } )
await metadataIndex . addToIndex ( id2 , { status : 'active' } )
await metadataIndex . flush ( )
// Verify both exist
let results = await metadataIndex . getIds ( 'status' , 'active' )
expect ( results . sort ( ) ) . toEqual ( [ id1 , id2 ] . sort ( ) )
// Remove one
await metadataIndex . removeFromIndex ( id1 , { status : 'active' } )
await metadataIndex . flush ( )
// Verify only one remains
results = await metadataIndex . getIds ( 'status' , 'active' )
expect ( results ) . toEqual ( [ id2 ] )
} )
} )
describe ( 'Performance Characteristics' , ( ) = > {
it ( 'should demonstrate memory efficiency' , ( ) = > {
// Create a Set with UUIDs
const uuidSet = new Set < string > ( )
for ( let i = 0 ; i < 1000 ; i ++ ) {
uuidSet . add ( uuidv4 ( ) )
}
// Estimate memory: 1000 UUIDs * 36 bytes = 36KB
const setMemory = uuidSet . size * 36
// Create a RoaringBitmap32 with integers
const bitmap = new RoaringBitmap32 ( )
for ( let i = 1 ; i <= 1000 ; i ++ ) {
bitmap . add ( i )
}
const bitmapMemory = bitmap . getSerializationSizeInBytes ( 'portable' )
// Roaring bitmap should be much smaller
expect ( bitmapMemory ) . toBeLessThan ( setMemory * 0.2 ) // Less than 20% of Set size
} )
it ( 'should demonstrate intersection speed advantage' , ( ) = > {
// Create large bitmaps
const bitmap1 = new RoaringBitmap32 ( )
const bitmap2 = new RoaringBitmap32 ( )
const bitmap3 = new RoaringBitmap32 ( )
for ( let i = 0 ; i < 10000 ; i ++ ) {
if ( i % 2 === 0 ) bitmap1 . add ( i )
if ( i % 3 === 0 ) bitmap2 . add ( i )
if ( i % 5 === 0 ) bitmap3 . add ( i )
}
// Measure roaring bitmap intersection
const start = performance . now ( )
const result = RoaringBitmap32 . and ( bitmap1 , bitmap2 , bitmap3 )
const roaringTime = performance . now ( ) - start
// Roaring intersection should be fast
expect ( roaringTime ) . toBeLessThan ( 10 ) // Under 10ms
// Result should contain numbers divisible by 2, 3, and 5 (i.e., divisible by 30)
// Verify a few samples
expect ( result . has ( 0 ) ) . toBe ( true )
expect ( result . has ( 30 ) ) . toBe ( true )
expect ( result . has ( 60 ) ) . toBe ( true )
expect ( result . size ) . toBeGreaterThan ( 0 )
} )
} )