fix(find): the projection seam is ES-private, and document the projection
TypeScript's `private` is compile-time only, so the seam's helpers were real prototype methods and the generated contract manifest listed them as public DOORS — which would have obliged every other engine to implement an internal detail. They are `#`-private now and the manifest is unchanged by this branch. Found while checking that: docs/api-contract.json was ALREADY stale at v10.4.11 — promoteQueuedFlush and startFlushLeader are in src and absent from the manifest, so they leaked the same way and were never re-emitted. Left alone here rather than folded into this branch; it is someone's to fix deliberately, and the fix is the same # conversion. docs/FIND_SYSTEM.md gains the projection: the rules, why a missing field is absent rather than an error, where the values come from and what a field the column cannot serve costs.
This commit is contained in:
parent
ad0f493f7a
commit
be77a10bfe
3 changed files with 117 additions and 29 deletions
|
|
@ -369,6 +369,71 @@ return results.slice(offset, offset + limit)
|
|||
// → Auto-correction: Use most likely alternative based on affinity data
|
||||
```
|
||||
|
||||
## Field Projection (`fields`)
|
||||
|
||||
`find()` and `get()` accept a `fields` list. Without it they return the whole
|
||||
record; with it they return only the fields you name — and, where the index can
|
||||
supply them, without opening the canonical record at all.
|
||||
|
||||
```ts
|
||||
// A list page: two user fields and one engine scalar. No document bodies.
|
||||
await brain.find({
|
||||
where: { kind: 'post' },
|
||||
fields: ['title', 'slug', 'system.createdAt'],
|
||||
limit: 50
|
||||
})
|
||||
|
||||
await brain.get(id, { fields: ['title'] })
|
||||
```
|
||||
|
||||
### Why it exists
|
||||
|
||||
A list view that renders a title and a date does not need the body, but without
|
||||
a projection every row hydrates its full record and throws almost all of it
|
||||
away. On a posts list that is the dominant cost of the query.
|
||||
|
||||
### The rules
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **`fields` absent** | The full record, byte-identical to before. Nothing changes. |
|
||||
| **Field names** | The one addressing law: a bare name is user metadata (`'title'`), `system.*` is an engine scalar (`'system.createdAt'`). |
|
||||
| **A field the row lacks** | Simply **absent** from the result. Never an error. |
|
||||
| **Identity** | Every row keeps its `id` (and `score` on `find`) regardless — a row you cannot identify is not a row. |
|
||||
| **Where values come from** | The **column store**, which holds raw values. Never the sparse index, which buckets timestamps for range queries. |
|
||||
| **A field the column cannot serve** | The canonical record is read for that field only. Correct, just not free. |
|
||||
|
||||
### Missing fields are absent, not errors
|
||||
|
||||
This is deliberate and differs from `orderBy`, which throws
|
||||
`UnresolvableFieldError` for an unknown field. A typo in `orderBy` silently
|
||||
changes the ordering, so it must be loud. A projection asks "give me these if
|
||||
you have them", and an optional field must not turn a list into a failure — so
|
||||
`fields` uses the permissive path.
|
||||
|
||||
### Cost
|
||||
|
||||
When every named field is column-served, a projected page performs **zero**
|
||||
canonical reads. When one is not, only that read happens and the rest still come
|
||||
from the index. Both are pinned by counting reads rather than timing them, in
|
||||
`tests/integration/find-fields-projection.test.ts`.
|
||||
|
||||
### `related()` takes no `fields`
|
||||
|
||||
A `Relation` carries `from` and `to` as **ids** and hydrates no entity record,
|
||||
so there is nothing for a projection to trim. Projecting the endpoints would be
|
||||
a new capability rather than a projection of an existing one.
|
||||
|
||||
### For engine implementers
|
||||
|
||||
Projection is served through an optional provider door,
|
||||
`getScalarsForIds(ids, fields)` on `MetadataIndexProvider`. The contract is in
|
||||
`src/plugin.ts`; the short version is **return only what you can serve exactly,
|
||||
and say what you served**. The caller diffs the answer against the request and
|
||||
reads records for the remainder, so omission costs a read while a wrong value is
|
||||
a wrong answer nobody can see. An engine without the door still works — every
|
||||
field falls back to the record.
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Query Performance by Type
|
||||
|
|
|
|||
Reference in a new issue