feat(query): the sparse-store cut — where on a never-carried field serves operator truth, never a refusal
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.
This commit is contained in:
parent
df96fccfd1
commit
7b67db4d0c
5 changed files with 188 additions and 8 deletions
|
|
@ -261,6 +261,28 @@ export function buildUnresolvableMessage(
|
|||
* the fix ships inside the error. Thrown by the query layer with index
|
||||
* knowledge, never by the pure parser.
|
||||
*/
|
||||
/**
|
||||
* Cross-package identity normalizer (the seam belt): the native accelerator
|
||||
* throws ITS OWN UnresolvableFieldError class, which fails `instanceof`
|
||||
* against this package's export — consumers were forced to match by name.
|
||||
* Every provider-boundary catch routes suspected field-refusals through
|
||||
* here: a foreign refusal (matched by name, duck fields tolerated) is
|
||||
* rethrown as THIS package's class, so exactly one identity ever reaches
|
||||
* consumers. Anything else returns null (caller rethrows the original).
|
||||
*/
|
||||
export function asBrainyFieldRefusal(err: unknown): UnresolvableFieldError | null {
|
||||
if (err instanceof UnresolvableFieldError) return err
|
||||
const e = err as { name?: string; message?: string; raw?: string; kind?: string } | null
|
||||
if (e && e.name === 'UnresolvableFieldError') {
|
||||
return new UnresolvableFieldError(
|
||||
e.raw ?? 'unknown-field',
|
||||
(e.kind as FieldAddressKind) ?? 'entity',
|
||||
e.message
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export class UnresolvableFieldError extends Error {
|
||||
public readonly raw: string
|
||||
public readonly kind: FieldAddressKind
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue