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).
This commit is contained in:
parent
62f6472fa0
commit
2427bb7960
17 changed files with 1164 additions and 328 deletions
|
|
@ -1,12 +1,14 @@
|
|||
/**
|
||||
* GraphAdjacencyIndex Pagination Tests (v5.8.0)
|
||||
* GraphAdjacencyIndex Pagination Tests (v5.8.0, updated for the 8.0 u64 contract)
|
||||
*
|
||||
* Tests for pagination support in GraphIndex methods:
|
||||
* - getNeighbors() with limit/offset
|
||||
* - getVerbIdsBySource() with limit/offset
|
||||
* - getVerbIdsByTarget() with limit/offset
|
||||
*
|
||||
* Verifies backward compatibility and new pagination features
|
||||
* 8.0 BigInt boundary: entity ints in (resolved via the metadata index's
|
||||
* idMapper), entity/verb ints out (`bigint[]`). Entity ints map back to UUIDs
|
||||
* via `idMapper.getUuid(Number(int))`; verb ints via `verbIntsToIds()`.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
|
|
@ -18,6 +20,25 @@ describe('GraphAdjacencyIndex Pagination', () => {
|
|||
let centralId: string
|
||||
let neighborIds: string[]
|
||||
|
||||
/** The brain's graph index (BigInt provider contract). */
|
||||
const graphIndex = () => (brain as any).graphIndex
|
||||
|
||||
/** The shared UUID ↔ int resolver. */
|
||||
const idMapper = () => (brain as any).metadataIndex.getIdMapper()
|
||||
|
||||
/** Resolve a UUID to its entity int (must already be mapped). */
|
||||
const entityInt = (uuid: string): bigint => {
|
||||
const intId = idMapper().getInt(uuid)
|
||||
expect(intId).toBeDefined()
|
||||
return BigInt(intId!)
|
||||
}
|
||||
|
||||
/** Map returned entity ints back to UUIDs. */
|
||||
const intsToUuids = (ints: bigint[]): string[] =>
|
||||
ints
|
||||
.map((i) => idMapper().getUuid(Number(i)))
|
||||
.filter((u: string | undefined): u is string => u !== undefined)
|
||||
|
||||
beforeEach(async () => {
|
||||
brain = new Brainy({ requireSubtype: false })
|
||||
await brain.init()
|
||||
|
|
@ -47,50 +68,42 @@ describe('GraphAdjacencyIndex Pagination', () => {
|
|||
})
|
||||
|
||||
describe('getNeighbors() Pagination', () => {
|
||||
it('should return all neighbors without pagination (backward compatible)', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
const neighbors = await graphIndex.getNeighbors(centralId)
|
||||
it('should return all neighbors without pagination', async () => {
|
||||
const neighborInts = await graphIndex().getNeighbors(entityInt(centralId))
|
||||
const neighbors = intsToUuids(neighborInts)
|
||||
|
||||
// Should return all 50 neighbors
|
||||
expect(neighbors).toHaveLength(50)
|
||||
expect(neighbors.every((id: string) => neighborIds.includes(id))).toBe(true)
|
||||
})
|
||||
|
||||
it('should return all outgoing neighbors with direction parameter (backward compatible)', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
// Old API: direction as string
|
||||
const neighbors = await graphIndex.getNeighbors(centralId, 'out')
|
||||
it('should return all outgoing neighbors with direction option', async () => {
|
||||
const neighbors = await graphIndex().getNeighbors(entityInt(centralId), { direction: 'out' })
|
||||
|
||||
expect(neighbors).toHaveLength(50)
|
||||
})
|
||||
|
||||
it('should paginate with limit only', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
const page1 = await graphIndex.getNeighbors(centralId, { limit: 10 })
|
||||
const page1Ints = await graphIndex().getNeighbors(entityInt(centralId), { limit: 10 })
|
||||
const page1 = intsToUuids(page1Ints)
|
||||
|
||||
expect(page1).toHaveLength(10)
|
||||
expect(page1.every((id: string) => neighborIds.includes(id))).toBe(true)
|
||||
})
|
||||
|
||||
it('should paginate with offset only', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
const all = await graphIndex.getNeighbors(centralId)
|
||||
const offsetResults = await graphIndex.getNeighbors(centralId, { offset: 10 })
|
||||
const all = await graphIndex().getNeighbors(entityInt(centralId))
|
||||
const offsetResults = await graphIndex().getNeighbors(entityInt(centralId), { offset: 10 })
|
||||
|
||||
// Offset 10 should return all after first 10
|
||||
expect(offsetResults).toHaveLength(all.length - 10)
|
||||
})
|
||||
|
||||
it('should paginate with both limit and offset', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
const page1 = await graphIndex.getNeighbors(centralId, { limit: 10, offset: 0 })
|
||||
const page2 = await graphIndex.getNeighbors(centralId, { limit: 10, offset: 10 })
|
||||
const page3 = await graphIndex.getNeighbors(centralId, { limit: 10, offset: 20 })
|
||||
const central = entityInt(centralId)
|
||||
const page1: bigint[] = await graphIndex().getNeighbors(central, { limit: 10, offset: 0 })
|
||||
const page2: bigint[] = await graphIndex().getNeighbors(central, { limit: 10, offset: 10 })
|
||||
const page3: bigint[] = await graphIndex().getNeighbors(central, { limit: 10, offset: 20 })
|
||||
|
||||
// Each page should have 10 results
|
||||
expect(page1).toHaveLength(10)
|
||||
|
|
@ -98,39 +111,32 @@ describe('GraphAdjacencyIndex Pagination', () => {
|
|||
expect(page3).toHaveLength(10)
|
||||
|
||||
// Pages should not overlap
|
||||
const page1Set = new Set(page1)
|
||||
const page2Set = new Set(page2)
|
||||
const page3Set = new Set(page3)
|
||||
|
||||
const overlap12 = page1.filter(id => page2Set.has(id))
|
||||
const overlap23 = page2.filter(id => page3Set.has(id))
|
||||
const overlap12 = page1.filter((id) => page2Set.has(id))
|
||||
const overlap23 = page2.filter((id) => page3Set.has(id))
|
||||
|
||||
expect(overlap12).toHaveLength(0)
|
||||
expect(overlap23).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle offset beyond total count', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
const results = await graphIndex.getNeighbors(centralId, { offset: 100 })
|
||||
const results = await graphIndex().getNeighbors(entityInt(centralId), { offset: 100 })
|
||||
|
||||
// Offset 100 > 50 total → empty array
|
||||
expect(results).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle limit = 0', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
const results = await graphIndex.getNeighbors(centralId, { limit: 0 })
|
||||
const results = await graphIndex().getNeighbors(entityInt(centralId), { limit: 0 })
|
||||
|
||||
expect(results).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should paginate with direction filter', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
// Get first 10 outgoing neighbors
|
||||
const page1 = await graphIndex.getNeighbors(centralId, {
|
||||
const page1 = await graphIndex().getNeighbors(entityInt(centralId), {
|
||||
direction: 'out',
|
||||
limit: 10,
|
||||
offset: 0
|
||||
|
|
@ -140,8 +146,6 @@ describe('GraphAdjacencyIndex Pagination', () => {
|
|||
})
|
||||
|
||||
it('should handle pagination with incoming direction', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
// Create some incoming relationships
|
||||
const sourceId = await brain.add({
|
||||
data: { name: 'Source' },
|
||||
|
|
@ -155,7 +159,7 @@ describe('GraphAdjacencyIndex Pagination', () => {
|
|||
})
|
||||
|
||||
// Get incoming neighbors with pagination
|
||||
const incoming = await graphIndex.getNeighbors(centralId, {
|
||||
const incoming = await graphIndex().getNeighbors(entityInt(centralId), {
|
||||
direction: 'in',
|
||||
limit: 10
|
||||
})
|
||||
|
|
@ -165,38 +169,36 @@ describe('GraphAdjacencyIndex Pagination', () => {
|
|||
})
|
||||
|
||||
describe('getVerbIdsBySource() Pagination', () => {
|
||||
it('should return all verb IDs without pagination (backward compatible)', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
it('should return all verb ints without pagination and resolve them back to ids', async () => {
|
||||
const verbInts: bigint[] = await graphIndex().getVerbIdsBySource(entityInt(centralId))
|
||||
|
||||
const verbIds = await graphIndex.getVerbIdsBySource(centralId)
|
||||
// Should return all 50 verb ints
|
||||
expect(verbInts).toHaveLength(50)
|
||||
|
||||
// Should return all 50 verb IDs
|
||||
// Every int must resolve back to a verb-id string
|
||||
const verbIds: (string | null)[] = await graphIndex().verbIntsToIds(verbInts)
|
||||
expect(verbIds).toHaveLength(50)
|
||||
expect(verbIds.every((id) => typeof id === 'string')).toBe(true)
|
||||
})
|
||||
|
||||
it('should paginate verb IDs with limit', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
const page1 = await graphIndex.getVerbIdsBySource(centralId, { limit: 10 })
|
||||
it('should paginate verb ints with limit', async () => {
|
||||
const page1 = await graphIndex().getVerbIdsBySource(entityInt(centralId), { limit: 10 })
|
||||
|
||||
expect(page1).toHaveLength(10)
|
||||
})
|
||||
|
||||
it('should paginate verb IDs with offset', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
const all = await graphIndex.getVerbIdsBySource(centralId)
|
||||
const offsetResults = await graphIndex.getVerbIdsBySource(centralId, { offset: 10 })
|
||||
it('should paginate verb ints with offset', async () => {
|
||||
const all = await graphIndex().getVerbIdsBySource(entityInt(centralId))
|
||||
const offsetResults = await graphIndex().getVerbIdsBySource(entityInt(centralId), { offset: 10 })
|
||||
|
||||
expect(offsetResults).toHaveLength(all.length - 10)
|
||||
})
|
||||
|
||||
it('should paginate verb IDs with limit and offset', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
const page1 = await graphIndex.getVerbIdsBySource(centralId, { limit: 10, offset: 0 })
|
||||
const page2 = await graphIndex.getVerbIdsBySource(centralId, { limit: 10, offset: 10 })
|
||||
const page3 = await graphIndex.getVerbIdsBySource(centralId, { limit: 10, offset: 20 })
|
||||
it('should paginate verb ints with limit and offset', async () => {
|
||||
const central = entityInt(centralId)
|
||||
const page1: bigint[] = await graphIndex().getVerbIdsBySource(central, { limit: 10, offset: 0 })
|
||||
const page2: bigint[] = await graphIndex().getVerbIdsBySource(central, { limit: 10, offset: 10 })
|
||||
const page3: bigint[] = await graphIndex().getVerbIdsBySource(central, { limit: 10, offset: 20 })
|
||||
|
||||
// Each page should have 10 results
|
||||
expect(page1).toHaveLength(10)
|
||||
|
|
@ -209,34 +211,28 @@ describe('GraphAdjacencyIndex Pagination', () => {
|
|||
expect(unique.size).toBe(30) // All unique
|
||||
})
|
||||
|
||||
it('should handle pagination edge cases for verb IDs', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
it('should handle pagination edge cases for verb ints', async () => {
|
||||
// Offset beyond total
|
||||
const beyondTotal = await graphIndex.getVerbIdsBySource(centralId, { offset: 100 })
|
||||
const beyondTotal = await graphIndex().getVerbIdsBySource(entityInt(centralId), { offset: 100 })
|
||||
expect(beyondTotal).toHaveLength(0)
|
||||
|
||||
// Limit = 0
|
||||
const zeroLimit = await graphIndex.getVerbIdsBySource(centralId, { limit: 0 })
|
||||
const zeroLimit = await graphIndex().getVerbIdsBySource(entityInt(centralId), { limit: 0 })
|
||||
expect(zeroLimit).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getVerbIdsByTarget() Pagination', () => {
|
||||
it('should return all verb IDs targeting an entity (backward compatible)', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
it('should return all verb ints targeting an entity', async () => {
|
||||
// Pick a neighbor that's a target of relationships
|
||||
const targetId = neighborIds[0]
|
||||
const verbIds = await graphIndex.getVerbIdsByTarget(targetId)
|
||||
const verbInts = await graphIndex().getVerbIdsByTarget(entityInt(targetId))
|
||||
|
||||
// Should have at least 1 (the relationship from central)
|
||||
expect(verbIds.length).toBeGreaterThanOrEqual(1)
|
||||
expect(verbInts.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('should paginate verb IDs by target with limit', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
it('should paginate verb ints by target with limit', async () => {
|
||||
// Create entity with many incoming relationships
|
||||
const popularTarget = await brain.add({
|
||||
data: { name: 'Popular Target' },
|
||||
|
|
@ -257,28 +253,29 @@ describe('GraphAdjacencyIndex Pagination', () => {
|
|||
}
|
||||
|
||||
// Paginate
|
||||
const page1 = await graphIndex.getVerbIdsByTarget(popularTarget, { limit: 10 })
|
||||
const page2 = await graphIndex.getVerbIdsByTarget(popularTarget, { limit: 10, offset: 10 })
|
||||
const target = entityInt(popularTarget)
|
||||
const page1: bigint[] = await graphIndex().getVerbIdsByTarget(target, { limit: 10 })
|
||||
const page2: bigint[] = await graphIndex().getVerbIdsByTarget(target, { limit: 10, offset: 10 })
|
||||
|
||||
expect(page1).toHaveLength(10)
|
||||
expect(page2).toHaveLength(10)
|
||||
|
||||
// No overlap
|
||||
const overlap = page1.filter((id: string) => page2.includes(id))
|
||||
const overlap = page1.filter((id) => page2.includes(id))
|
||||
expect(overlap).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Performance with Pagination', () => {
|
||||
it('should maintain sub-5ms performance with pagination', async () => {
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
const central = entityInt(centralId)
|
||||
|
||||
// Multiple paginated queries should be fast
|
||||
const startTime = performance.now()
|
||||
|
||||
await graphIndex.getNeighbors(centralId, { limit: 10, offset: 0 })
|
||||
await graphIndex.getNeighbors(centralId, { limit: 10, offset: 10 })
|
||||
await graphIndex.getNeighbors(centralId, { limit: 10, offset: 20 })
|
||||
await graphIndex().getNeighbors(central, { limit: 10, offset: 0 })
|
||||
await graphIndex().getNeighbors(central, { limit: 10, offset: 10 })
|
||||
await graphIndex().getNeighbors(central, { limit: 10, offset: 20 })
|
||||
|
||||
const elapsed = performance.now() - startTime
|
||||
|
||||
|
|
@ -310,12 +307,11 @@ describe('GraphAdjacencyIndex Pagination', () => {
|
|||
})
|
||||
}
|
||||
|
||||
const graphIndex = (brain as any).graphIndex
|
||||
|
||||
// Paginate in chunks of 25
|
||||
const chunks: string[][] = []
|
||||
const hubInt = entityInt(hub)
|
||||
const chunks: bigint[][] = []
|
||||
for (let offset = 0; offset < 100; offset += 25) {
|
||||
const chunk = await graphIndex.getNeighbors(hub, {
|
||||
const chunk = await graphIndex().getNeighbors(hubInt, {
|
||||
direction: 'out',
|
||||
limit: 25,
|
||||
offset
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue