The provider's read doors each serve one stage, so a find that consults three of them crosses into the index three times and marshals a result set at every crossing: a filter matching a hundred thousand rows builds a hundred thousand id strings to return a page of twenty-five. An index able to decide the stage order itself can answer the page in one call and build ids only for the page. planFindPage is optional and additive, in the shape filterIdsWithin and getIdSetForFilter already set. The hook sits above the branch selection, because the branches are what decide stage order per call site and an index that plans has to be asked before that choice is made. Absent — as it is on this engine's own index — every find is served by the stage doors exactly as before, which is what keeps this engine the ordering oracle for any index that implements one. The contract the door must keep, written where an implementer will read it: identical rows in identical order to what the stage doors would produce; the graph-first law (neighbours are the candidate universe, the filter runs over those ids, orderBy sorts the whole set, the page is cut last); null returned BEFORE any work rather than instead of an answer; and emptyAt naming the stage that produced an empty page, so the serving law is applied to the right index — an empty graph answer is re-verified against the adjacency before it is believed, and a filter-empty is not. Pinned in tests/integration/find-planner-door.test.ts: absent changes nothing; present it is asked first with normalized params, the hidden ids and the graph provider; its page is used and hydrated in its order; a declining door leaves the result identical to the no-door path; and the two emptyAt branches verify the adjacency, or correctly do not.
137 lines
5.2 KiB
TypeScript
137 lines
5.2 KiB
TypeScript
/**
|
|
* @module tests/integration/find-planner-door
|
|
* @description The optional `MetadataIndexProvider.planFindPage` door.
|
|
*
|
|
* The stage doors each serve one stage, so a `find()` that consults three of
|
|
* them crosses into the index three times and marshals a result set at every
|
|
* crossing — a filter matching a hundred thousand rows builds a hundred
|
|
* thousand id strings to return a page of twenty-five. An index that can decide
|
|
* the stage order itself answers the page in one call.
|
|
*
|
|
* These pins hold the three properties that make such a door safe to add:
|
|
*
|
|
* 1. **Absent, nothing changes.** The reference index has no planner, and every
|
|
* find is served by the stage doors exactly as before. That is also what
|
|
* makes this engine the ordering oracle for any index that implements one.
|
|
* 2. **Present, it is asked first and its answer is used** — above the branch
|
|
* selection, with the params already normalized, the hidden ids passed, and
|
|
* the graph provider handed over.
|
|
* 3. **`null` is routing, not an answer.** A door that declines a shape leaves
|
|
* it to the path that always served it, and the result is unchanged.
|
|
*
|
|
* Plus the serving law: an empty page stamped `emptyAt: 'graph'` is re-verified
|
|
* against the adjacency before it is believed, so a not-serving graph refuses
|
|
* loudly instead of answering `[]` as truth.
|
|
*/
|
|
import { describe, it, expect, beforeAll, vi } from 'vitest'
|
|
import { Brainy } from '../../src/brainy'
|
|
import { NounType, VerbType } from '../../src/types/graphTypes'
|
|
import { generateTestVector } from '../helpers/test-factory'
|
|
|
|
describe('find(): the optional planner door', () => {
|
|
let brain: Brainy<any>
|
|
const anchor = 'planner-anchor'
|
|
let neighbourId = ''
|
|
|
|
beforeAll(async () => {
|
|
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
|
await brain.init()
|
|
await brain.add({
|
|
id: anchor,
|
|
data: 'anchor',
|
|
type: NounType.Person,
|
|
metadata: { kind: 'anchor' },
|
|
vector: generateTestVector()
|
|
})
|
|
for (let i = 0; i < 12; i++) {
|
|
const id = await brain.add({
|
|
id: `row-${i}`,
|
|
data: `row ${i}`,
|
|
type: NounType.Person,
|
|
metadata: { kind: 'note', rank: i },
|
|
vector: generateTestVector()
|
|
})
|
|
if (i === 0) neighbourId = id
|
|
await brain.relate({ from: anchor, to: id, type: VerbType.Knows })
|
|
}
|
|
})
|
|
|
|
/** Install a planner door for one call, then remove it. */
|
|
const withDoor = async <T>(
|
|
door: (...a: any[]) => Promise<any>,
|
|
body: () => Promise<T>
|
|
): Promise<T> => {
|
|
const index = (brain as any).metadataIndex
|
|
index.planFindPage = door
|
|
try {
|
|
return await body()
|
|
} finally {
|
|
delete index.planFindPage
|
|
}
|
|
}
|
|
|
|
it('is absent on the reference index — every find is served by the stage doors', async () => {
|
|
expect((brain as any).metadataIndex.planFindPage).toBeUndefined()
|
|
const results = await brain.find({ where: { kind: 'note' }, limit: 5 })
|
|
expect(results).toHaveLength(5)
|
|
})
|
|
|
|
it('is asked before the branches, with normalized params and the graph provider', async () => {
|
|
const door = vi.fn(async () => null)
|
|
await withDoor(door, async () => {
|
|
await brain.find({ where: { kind: 'note' }, limit: 5 })
|
|
})
|
|
expect(door).toHaveBeenCalledTimes(1)
|
|
const [params, hidden, graph] = door.mock.calls[0] as any[]
|
|
expect(params.where).toEqual({ kind: 'note' })
|
|
expect(Array.isArray(hidden)).toBe(true)
|
|
expect(graph).toBe((brain as any).graphIndex)
|
|
})
|
|
|
|
it('uses the page it answers, hydrated and in the door\'s order', async () => {
|
|
const results = await withDoor(
|
|
async () => ({ ids: [neighbourId], emptyAt: 'none' as const }),
|
|
async () => brain.find({ where: { kind: 'note' }, limit: 5 })
|
|
)
|
|
expect(results).toHaveLength(1)
|
|
expect(results[0].entity.id).toBe(neighbourId)
|
|
})
|
|
|
|
it('a declining door changes nothing — the shape is served as it always was', async () => {
|
|
const withoutDoor = await brain.find({ where: { kind: 'note' }, orderBy: 'rank', limit: 4 })
|
|
const declined = await withDoor(
|
|
async () => null,
|
|
async () => brain.find({ where: { kind: 'note' }, orderBy: 'rank', limit: 4 })
|
|
)
|
|
expect(declined.map((r) => r.entity.id)).toEqual(withoutDoor.map((r) => r.entity.id))
|
|
})
|
|
|
|
it('re-verifies the adjacency before believing an empty graph answer', async () => {
|
|
const verify = vi.spyOn(brain as any, 'verifyGraphAdjacencyLive')
|
|
try {
|
|
const results = await withDoor(
|
|
async () => ({ ids: [], emptyAt: 'graph' as const }),
|
|
async () => brain.find({ connected: { from: anchor }, where: { kind: 'note' }, limit: 5 })
|
|
)
|
|
expect(results).toEqual([])
|
|
expect(verify).toHaveBeenCalled()
|
|
} finally {
|
|
verify.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('does not re-verify the adjacency for an empty the FILTER produced', async () => {
|
|
const verify = vi.spyOn(brain as any, 'verifyGraphAdjacencyLive')
|
|
verify.mockClear()
|
|
try {
|
|
const results = await withDoor(
|
|
async () => ({ ids: [], emptyAt: 'filter' as const }),
|
|
async () => brain.find({ where: { kind: 'note' }, limit: 5 })
|
|
)
|
|
expect(results).toEqual([])
|
|
expect(verify).not.toHaveBeenCalled()
|
|
} finally {
|
|
verify.mockRestore()
|
|
}
|
|
})
|
|
})
|