A first adopter's namespace migration went 341 red on one class: the never-carried-field refusal firing on CORRECT filters against fresh and sparse stores — a freshly provisioned tenant refused its own first filtered read, with the did-you-mean built for typos firing hardest on day-one stores where nothing is wrong. The ruled cut: a WHERE filter naming a field no row carries is SERVED OPERATOR-TRUTHFULLY — eq/in/range/contains answer [] (nothing carries it, nothing matches); ne and exists:false answer ALL rows (the equally true complement — a blanket empty here would be silently wrong, which is why the simpler cut was rejected); exists:true answers []. Served from the field registry, with the did-you-mean demoted to a once-per-field WARN. orderBy and genuinely ambiguous addresses KEEP their hard typed refusals: no truthful order exists over an uncarried field, and ambiguity is a contract error while absence is data. Mechanics: the negative operator absorbs the FIELD_NOT_INDEXED throw as its empty exclude set (the clause-level catch correctly zeroes positive operators only); the egress matcher already agreed. Plus the provider-seam belt: a field refusal thrown by a replacement metadata manager is normalized to THIS package's UnresolvableFieldError at every filter call site — one class identity for consumers, instanceof works (a first adopter's cross-package finding). Conformance: tests/conformance/sparse-store-cut.test.ts — the shared operator rows both engines run (positive-empty, negative-all, fresh-tenant day-one, orderBy refusal kept, compound composition). Gates: unit 2065/2065 · integration 832 · conformance 36/36.
82 lines
3.7 KiB
TypeScript
82 lines
3.7 KiB
TypeScript
/**
|
|
* @module tests/conformance/sparse-store-cut
|
|
* @description THE SPARSE-STORE CUT (ruled 2026-08-12) — the shared
|
|
* conformance rows both engines run: a WHERE filter naming a field NO row
|
|
* carries is SERVED OPERATOR-TRUTHFULLY, never refused:
|
|
* eq / in / range / contains → [] (nothing carries it → nothing matches)
|
|
* ne / exists:false → ALL rows (equally true — blanket-empty here
|
|
* would be the outlawed silent wrong)
|
|
* exists:true → []
|
|
* orderBy on an unresolvable field KEEPS the hard refusal (no truthful
|
|
* order exists). The did-you-mean demotes to a throttled WARN on the serve.
|
|
* A fresh tenant's first filtered read is a correct empty answer — the
|
|
* 341-red first-adopter class, closed.
|
|
*/
|
|
import { describe, it, expect, afterEach } from 'vitest'
|
|
import { Brainy, UnresolvableFieldError } from '../../src/index.js'
|
|
import { NounType } from '../../src/types/graphTypes.js'
|
|
|
|
const brains: Brainy[] = []
|
|
afterEach(async () => {
|
|
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
|
})
|
|
|
|
async function corpus(): Promise<{ brain: Brainy; ids: string[] }> {
|
|
const b = new Brainy({ storage: { type: 'memory' }, requireSubtype: false })
|
|
await b.init()
|
|
brains.push(b)
|
|
const ids: string[] = []
|
|
for (let i = 0; i < 4; i++) {
|
|
ids.push(
|
|
await b.add({ data: `row ${i}`, type: NounType.Document, metadata: { carried: i } })
|
|
)
|
|
}
|
|
return { brain: b, ids }
|
|
}
|
|
|
|
describe('sparse-store cut — operator-truthful serve on never-carried fields', () => {
|
|
it('positive matches serve EMPTY: eq, in, range, contains', async () => {
|
|
const { brain } = await corpus()
|
|
expect(await brain.find({ where: { ghost: 'x' }, limit: 10 })).toEqual([])
|
|
expect(await brain.find({ where: { ghost: { in: ['a', 'b'] } }, limit: 10 })).toEqual([])
|
|
expect(await brain.find({ where: { ghost: { gt: 5 } }, limit: 10 })).toEqual([])
|
|
expect(await brain.find({ where: { ghost: { exists: true } }, limit: 10 })).toEqual([])
|
|
})
|
|
|
|
it('negative matches serve ALL rows: ne and exists:false (the truth, not blanket-empty)', async () => {
|
|
const { brain, ids } = await corpus()
|
|
const ne = await brain.find({ where: { ghost: { ne: 'x' } }, limit: 10 })
|
|
expect(ne.map((r) => r.id).sort()).toEqual([...ids].sort())
|
|
const absent = await brain.find({ where: { ghost: { exists: false } }, limit: 10 })
|
|
expect(absent.map((r) => r.id).sort()).toEqual([...ids].sort())
|
|
})
|
|
|
|
it('the fresh-tenant day-one shape: an EMPTY store answers its first filtered read with [], never a refusal', async () => {
|
|
const b = new Brainy({ storage: { type: 'memory' }, requireSubtype: false })
|
|
await b.init()
|
|
brains.push(b)
|
|
expect(await b.find({ where: { status: 'open' }, limit: 50 })).toEqual([])
|
|
expect(await b.find({ where: { date: { gte: '2026-01-01' } }, limit: 50 })).toEqual([])
|
|
})
|
|
|
|
it('orderBy on an unresolvable field KEEPS the typed refusal', async () => {
|
|
const { brain } = await corpus()
|
|
await expect(
|
|
brain.find({ where: { carried: { gte: 0 } }, orderBy: 'system.notAScalar', limit: 10 })
|
|
).rejects.toThrow(UnresolvableFieldError)
|
|
})
|
|
|
|
it('compound filters: the never-carried clause composes truthfully with carried clauses', async () => {
|
|
const { brain, ids } = await corpus()
|
|
// carried>=2 AND ghost ne 'x' → the carried>=2 rows (ne-clause = all).
|
|
const both = await brain.find({
|
|
where: { carried: { gte: 2 }, ghost: { ne: 'x' } },
|
|
limit: 10
|
|
})
|
|
expect(both.map((r) => r.id).sort()).toEqual([ids[2], ids[3]].sort())
|
|
// carried>=2 AND ghost eq 'x' → [] (eq-clause empties the intersection).
|
|
expect(
|
|
await brain.find({ where: { carried: { gte: 2 }, ghost: 'x' }, limit: 10 })
|
|
).toEqual([])
|
|
})
|
|
})
|