2026-05-28 10:24:48 -07:00
/ * *
* @module column - store - interchange . test
* @description 2.4 . 0 # 4 — column - store JS ↔ native interchange . Pins down that
* brainy ' s JS ` ColumnStore ` writes segments + DELETED bitmaps via the binary -
* blob primitive at the cortex - shared key convention , AND that indexes
* persisted under the pre - 2.4 . 0 envelope ( ` { _binary, base64 } ` at the
* ` .cidx ` object - path ) still load correctly via the legacy - read fallback .
*
* Together these properties make the JS and native engines byte - compatible
* at the segment payload , so writing through one and reading through the
* other is a no - op format - wise — closing the gap the cortex 2.3 . 1 read - side
* fallback opened on the cortex side .
* /
import { describe , it , expect , beforeEach , afterEach } from 'vitest'
import { ColumnStore } from '../../../../src/indexes/columnStore/ColumnStore.js'
import { MemoryStorage } from '../../../../src/storage/adapters/memoryStorage.js'
import { EntityIdMapper } from '../../../../src/utils/entityIdMapper.js'
import { writeSegmentToBuffer } from '../../../../src/indexes/columnStore/ColumnSegmentFormat.js'
import { ColumnManifest } from '../../../../src/indexes/columnStore/ColumnManifest.js'
import { RoaringBitmap32 } from '../../../../src/utils/roaring/index.js'
describe ( 'ColumnStore — JS↔native interchange (2.4.0 #4)' , ( ) = > {
let storage : MemoryStorage
let idMapper : EntityIdMapper
let store : ColumnStore
beforeEach ( async ( ) = > {
storage = new MemoryStorage ( )
await storage . init ( )
idMapper = new EntityIdMapper ( { storage , storageKey : 'test:idMapper' } )
await idMapper . init ( )
store = new ColumnStore ( { flushThreshold : 10 } )
await store . init ( storage , idMapper )
} )
afterEach ( async ( ) = > {
await store . close ( )
} )
// -----------------------------------------------------------------------
// WRITE side: raw blob at the cortex-shared key, no legacy envelope
// -----------------------------------------------------------------------
it ( 'flushBuffer writes segment bytes as a raw blob at the cortex-shared key' , async ( ) = > {
for ( let i = 0 ; i < 5 ; i ++ ) {
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
store . addEntity ( BigInt ( idMapper . getOrAssign ( ` u- ${ i } ` ) ) , { score : i * 10 } )
2026-05-28 10:24:48 -07:00
}
await store . flush ( )
// The raw blob exists at the suffix-free cortex key.
const key = '_column_index/score/L0-000001'
const raw = await ( storage as any ) . loadBinaryBlob ( key )
expect ( Buffer . isBuffer ( raw ) ) . toBe ( true )
expect ( raw ! . length ) . toBeGreaterThan ( 0 )
// The legacy envelope at the `.cidx` object-path is NOT written (no
// 33% base64 bloat, no dual-write).
const legacyPath = '_column_index/score/L0-000001.cidx'
const legacyEnvelope = await ( storage as any ) . readObjectFromPath ( legacyPath )
expect ( legacyEnvelope ) . toBeFalsy ( )
} )
it ( 'flushBuffer writes the DELETED bitmap as a raw blob too (not an envelope)' , async ( ) = > {
for ( let i = 0 ; i < 3 ; i ++ ) {
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
store . addEntity ( BigInt ( idMapper . getOrAssign ( ` u- ${ i } ` ) ) , { score : i } )
2026-05-28 10:24:48 -07:00
}
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
store . removeEntity ( BigInt ( idMapper . getInt ( 'u-1' ) ! ) )
2026-05-28 10:24:48 -07:00
await store . flush ( )
const rawDel = await ( storage as any ) . loadBinaryBlob ( '_column_index/score/DELETED' )
expect ( Buffer . isBuffer ( rawDel ) ) . toBe ( true )
expect ( rawDel ! . length ) . toBeGreaterThan ( 0 )
const legacyEnvelope = await ( storage as any ) . readObjectFromPath (
'_column_index/score/DELETED.bin'
)
expect ( legacyEnvelope ) . toBeFalsy ( )
} )
// -----------------------------------------------------------------------
// READ side: pre-2.4.0 legacy envelope still loads via the fallback
// -----------------------------------------------------------------------
it ( 'loads a segment persisted as the pre-2.4.0 envelope (legacy fallback)' , async ( ) = > {
// Seed a legacy-format index DIRECTLY on storage — the on-disk shape
// brainy 7.20.0..7.25.0 wrote before this commit.
const legacyStore = new ColumnStore ( )
await legacyStore . init ( storage , idMapper )
// Build a segment buffer using the shared cidx format.
const values : number [ ] = [ 10 , 20 , 30 ]
const entityIds : number [ ] = [
idMapper . getOrAssign ( 'legacy-a' ) ,
idMapper . getOrAssign ( 'legacy-b' ) ,
idMapper . getOrAssign ( 'legacy-c' )
]
const segBuffer = writeSegmentToBuffer (
{
fieldName : 'score' ,
fieldNameLength : 5 ,
valueType : 0 ,
level : 0 ,
codec : 0 ,
flags : 0 ,
count : values.length
} ,
values ,
entityIds
)
// Persist via the LEGACY envelope path (writeObjectToPath, base64 wrap).
await ( storage as any ) . writeObjectToPath ( '_column_index/score/L0-000001.cidx' , {
_binary : true ,
data : segBuffer.toString ( 'base64' )
} )
const manifest = new ColumnManifest ( 'score' , '_column_index' )
manifest . valueType = 0
manifest . multiValue = false
manifest . addSegment ( {
id : 1 ,
level : 0 ,
count : values.length ,
minValue : values [ 0 ] ,
maxValue : values [ values . length - 1 ] ,
file : 'L0-000001.cidx'
} )
await manifest . save ( storage )
await legacyStore . close ( )
// NEW ColumnStore re-discovers the manifest at init, then the legacy
// segment loads via the readObjectFromPath fallback (no raw blob exists
// at the cortex key, so it falls through). sortTopK returns the entities
// in the order the segment recorded — proves the bytes round-tripped.
const readStore = new ColumnStore ( )
await readStore . init ( storage , idMapper )
const sortedInts = await readStore . sortTopK ( 'score' , 'asc' , 10 )
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
const sortedUuids = sortedInts . map ( i = > idMapper . getUuid ( Number ( i ) ) )
2026-05-28 10:24:48 -07:00
expect ( sortedUuids ) . toEqual ( [ 'legacy-a' , 'legacy-b' , 'legacy-c' ] )
await readStore . close ( )
} )
it ( 'loads a DELETED bitmap persisted as the pre-2.4.0 envelope (legacy fallback)' , async ( ) = > {
// Manually seed a legacy DELETED.bin envelope, then re-init and confirm
// the entity it marks gone is excluded from filter/sort results.
const bm = new RoaringBitmap32 ( )
bm . add ( idMapper . getOrAssign ( 'dead' ) )
const serialized = Buffer . from ( bm . serialize ( true ) )
await ( storage as any ) . writeObjectToPath ( '_column_index/score/DELETED.bin' , {
_binary : true ,
data : serialized.toString ( 'base64' )
} )
// Add a couple of live entities so a manifest exists (init only loads
// DELETED for fields with a manifest on disk).
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
store . addEntity ( BigInt ( idMapper . getOrAssign ( 'alive-1' ) ) , { score : 100 } )
store . addEntity ( BigInt ( idMapper . getOrAssign ( 'alive-2' ) ) , { score : 200 } )
2026-05-28 10:24:48 -07:00
await store . flush ( )
await store . close ( )
const readStore = new ColumnStore ( )
await readStore . init ( storage , idMapper )
const sortedInts = await readStore . sortTopK ( 'score' , 'asc' , 10 )
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
const sortedUuids = sortedInts . map ( i = > idMapper . getUuid ( Number ( i ) ) )
2026-05-28 10:24:48 -07:00
// The dead entity must NOT appear; the alive ones must.
expect ( sortedUuids ) . not . toContain ( 'dead' )
expect ( sortedUuids ) . toContain ( 'alive-1' )
expect ( sortedUuids ) . toContain ( 'alive-2' )
await readStore . close ( )
} )
// -----------------------------------------------------------------------
// ROUND-TRIP: write-then-read in the new format
// -----------------------------------------------------------------------
it ( 'write (new format) → re-init → read returns the same data' , async ( ) = > {
for ( let i = 0 ; i < 5 ; i ++ ) {
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
store . addEntity ( BigInt ( idMapper . getOrAssign ( ` u- ${ i } ` ) ) , { score : i * 10 } )
2026-05-28 10:24:48 -07:00
}
await store . flush ( )
await store . close ( )
const readStore = new ColumnStore ( )
await readStore . init ( storage , idMapper )
const sortedInts = await readStore . sortTopK ( 'score' , 'asc' , 10 )
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
const sortedUuids = sortedInts . map ( i = > idMapper . getUuid ( Number ( i ) ) )
2026-05-28 10:24:48 -07:00
expect ( sortedUuids ) . toEqual ( [ 'u-0' , 'u-1' , 'u-2' , 'u-3' , 'u-4' ] )
await readStore . close ( )
} )
} )