From 905c267c47a9515abcde4a713b3c575ec730e7b2 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Wed, 2 Sep 2026 09:28:44 -0700 Subject: [PATCH] fix(find): a page the metadata block already cut is not cut again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `find({ query, connected, where, offset })` answered [] for every page but the first. The metadata block ranks the fused candidates and CUTS the page itself — rows [offset, offset+limit) — and then returns early. Two shapes do not take that early return, `connected` and `fusion`, and they fell through to the tail, which sliced the already-cut page by `offset` a second time: a five-row page sliced at offset five is nothing at all. Every page after the first was empty, and the caller had no way to tell that from "no more rows". The block now records that it consumed the offset, and the tail returns the page it was handed instead of re-cutting it. Nothing changes at offset 0, where the second slice was the identity. Pinned in tests/integration/find-hybrid-filter-before-hydrate.test.ts: page two of a `connected` hybrid find matches the pipeline oracle row for row, paging reaches every matching neighbour exactly once, and a `fusion` find's second page is the same page the plain find returns. --- src/brainy.ts | 15 ++++- .../find-hybrid-filter-before-hydrate.test.ts | 55 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/brainy.ts b/src/brainy.ts index ad8bbf90..ffc2d5cd 100644 --- a/src/brainy.ts +++ b/src/brainy.ts @@ -7658,6 +7658,11 @@ export class Brainy implements BrainyInterface { // the hybrid branch, so every other path hydrates unchanged. let finishHybridRow: ((row: Result, pending: Result) => void) | undefined + // Set once the metadata block below has already ranked and CUT the page. + // The tail must not cut it a second time: `offset` has been consumed, and + // re-slicing a `limit`-long page by `offset` returns nothing at all. + let pagedEarly = false + // Handle text-only query (user explicitly wants text search) if (searchMode === 'text' && params.query && params.query.trim() !== '') { results = await this.executeTextSearch(params.query, limit * 2) @@ -7754,6 +7759,7 @@ export class Brainy implements BrainyInterface { const k = offset + limit const order = rankIndicesByScore(results.map(r => r.score), k, true) results = reorderByIndices(results, order).slice(offset, k) + pagedEarly = true // Batch-load entities only for the paginated results (10x faster on GCS). // This is the hydrate-last seam for the deferring paths: a row that @@ -7868,8 +7874,15 @@ export class Brainy implements BrainyInterface { // Efficient pagination - only slice what we need (limit already defined // above), THEN read canonical for the page. Rows that arrived hydrated // pass straight through; a deferred path reads exactly these rows. + // + // A page the metadata block already cut is NOT cut again: it holds the + // rows at [offset, offset+limit) of the ranking, so slicing it by + // `offset` a second time drops the whole page. That is how + // `find({ query, connected, where, offset })` — the shapes that reach + // here after early paging, `connected` and `fusion` — answered [] for + // every page but the first. return await this.hydrateResultPage( - results.slice(finalOffset, finalOffset + limit), + pagedEarly ? results : results.slice(finalOffset, finalOffset + limit), finishHybridRow ) })() diff --git a/tests/integration/find-hybrid-filter-before-hydrate.test.ts b/tests/integration/find-hybrid-filter-before-hydrate.test.ts index 252fe89b..3e74f5d8 100644 --- a/tests/integration/find-hybrid-filter-before-hydrate.test.ts +++ b/tests/integration/find-hybrid-filter-before-hydrate.test.ts @@ -360,6 +360,61 @@ describe('hybrid find: filter before hydrate — the answer is unchanged', () => for (const r of actual) expect(neighbours.has(r.id)).toBe(true) }) + it('hybrid + connected + offset: page two is the page, not an empty answer', async () => { + const params = { + query: QUERY, + connected: { from: anchor, direction: 'out' as const }, + where: { lane: 'alpha' }, + limit: 5, + offset: 5 + } + const expected = await legacyHybridFind(brain as any, params) + expect(expected).toHaveLength(5) + const actual = await brain.find(params as any) + expect(actual.length).toBe(expected.length) + expect(project(actual)).toEqual(expected) + }) + + it('hybrid + connected: paging reaches every matching neighbour exactly once', async () => { + const seen = new Set() + for (let offset = 0; offset < MATCHES; offset += 6) { + const page = await brain.find({ + query: QUERY, + connected: { from: anchor, direction: 'out' as const }, + where: { lane: 'alpha' }, + limit: 6, + offset + } as any) + for (const r of page) { + expect(seen.has(r.id)).toBe(false) + seen.add(r.id) + } + } + // Every row the fused candidate set holds is reachable by paging, and the + // neighbour set is the ceiling. + expect(seen.size).toBeGreaterThanOrEqual(MATCHES) + const neighbours = new Set(matchIds) + for (const id of seen) expect(neighbours.has(id)).toBe(true) + }) + + it('hybrid + fusion + offset: page two is the page', async () => { + const plain = await brain.find({ + query: QUERY, + where: { lane: 'alpha' }, + limit: 5, + offset: 5 + } as any) + const fused = await brain.find({ + query: QUERY, + where: { lane: 'alpha' }, + fusion: 'weighted', + limit: 5, + offset: 5 + } as any) + expect(fused).toHaveLength(plain.length) + expect(fused.map((r) => r.id)).toEqual(plain.map((r) => r.id)) + }) + it('a hydrated hybrid row is shaped exactly as an eagerly-built one', async () => { const rows = await brain.find({ query: QUERY, where: { lane: 'alpha' }, limit: 8 } as any) const row = rows[0]