feat(find): field projection — fields resolve from the column store, not the record
A list view that shows a title and a slug hydrates the whole record for every
row, document bodies included, and discards almost all of it. find/get({ fields })
names what is wanted; the column store serves it; the canonical record is opened
only for fields the index cannot supply.
The provider grows an optional getScalarsForIds(ids, fields) door, batched: it
walks each column ONCE and picks out every requested id, rather than re-walking
per row. The column store grows the primitive that was missing — valuesForIds —
because every other read door there answers which entities have a value, and a
projection asks the opposite.
It reads the COLUMN store, never the sparse index: the column keeps raw values,
the sparse index keeps a bucketed form built for range queries, and a projection
served from the latter would return a value that differs from the record's. A
field the column cannot serve is omitted rather than approximated — omission
costs a read, a wrong value is a wrong answer nobody can see.
Two laws the pins hold: fields absent is byte-identical to today, and a missing
field is simply absent rather than an error — so this path deliberately avoids
the strict address resolver, whose UnresolvableFieldError is right for orderBy
and wrong here.
related() takes no fields: a Relation carries from/to as ids and hydrates no
record, so the param would be decorative.
This commit is contained in:
parent
6597c146f7
commit
ad0f493f7a
7 changed files with 639 additions and 7 deletions
|
|
@ -495,6 +495,45 @@ export interface MetadataIndexProvider {
|
|||
query: string,
|
||||
ids: readonly string[]
|
||||
): Promise<Array<{ id: string; matchCount: number }>>
|
||||
/**
|
||||
* @description OPTIONAL: read named SCALAR fields for many ids at once, from
|
||||
* the index's own value storage, WITHOUT touching the canonical record.
|
||||
*
|
||||
* This is the door behind `find/get/related({ fields })`. A list view that
|
||||
* needs a title and a slug currently hydrates the whole record for every row
|
||||
* — document bodies included — and then discards almost all of it. Serving
|
||||
* the named scalars from the index turns that into an index read.
|
||||
*
|
||||
* ## The contract, and the one rule that makes it safe
|
||||
*
|
||||
* **Return only what you can serve EXACTLY, and say what you served.** The
|
||||
* answer is a per-id map of the fields this index actually resolved; the
|
||||
* caller diffs it against what was requested and reads the canonical record
|
||||
* for the remainder. An implementation must therefore OMIT a field rather
|
||||
* than approximate it — and omission costs only a record read, while a wrong
|
||||
* value is a wrong answer nobody can see.
|
||||
*
|
||||
* That rule is not hypothetical. This engine's own index buckets
|
||||
* `system.createdAt` and `system.updatedAt` to the minute for range queries,
|
||||
* so it cannot serve them exactly and omits them. An engine whose column
|
||||
* store holds raw values can serve the same fields — so the two answer
|
||||
* differently in COST and identically in CONTENT, which is the only
|
||||
* difference a projection door is allowed to have.
|
||||
*
|
||||
* A field absent from an entity is simply absent from that entity's map. It
|
||||
* is never an error, and never a `null` standing in for one: absent and
|
||||
* present-and-null are different answers.
|
||||
*
|
||||
* @param ids - Canonical entity ids to read.
|
||||
* @param fields - Index KEYS (bare = user metadata, `system.*` = engine
|
||||
* scalar), already address-resolved by the caller.
|
||||
* @returns `id → { field: value }` for the fields this index served exactly.
|
||||
* Ids with nothing to serve may be omitted entirely.
|
||||
*/
|
||||
getScalarsForIds?(
|
||||
ids: readonly string[],
|
||||
fields: readonly string[]
|
||||
): Promise<Map<string, Record<string, unknown>>>
|
||||
getSortedIdsForFilter(filter: any, orderBy: string, order?: 'asc' | 'desc', topK?: number): Promise<string[]>
|
||||
getFilterValues(field: string): Promise<string[]>
|
||||
getFilterFields(): Promise<string[]>
|
||||
|
|
|
|||
Reference in a new issue