197 lines
8.4 KiB
Markdown
197 lines
8.4 KiB
Markdown
|
|
---
|
|||
|
|
title: Field addressing: your fields and system fields
|
|||
|
|
slug: concepts/field-addressing
|
|||
|
|
public: true
|
|||
|
|
category: concepts
|
|||
|
|
template: concept
|
|||
|
|
order: 7
|
|||
|
|
description: The one rule for every query-surface field name — a bare name always means your metadata, system.<field> reaches the ten engine scalars explicitly, and anything else refuses by name.
|
|||
|
|
next:
|
|||
|
|
- concepts/consistency-model
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Field addressing: your fields and system fields
|
|||
|
|
|
|||
|
|
Every query surface in Brainy — `find()`'s `where`, `orderBy`, aggregation
|
|||
|
|
`groupBy`, and aggregation `source.where` — resolves field names by one rule,
|
|||
|
|
with no exceptions:
|
|||
|
|
|
|||
|
|
> **A bare field name always means your metadata. `system.<field>` reaches an
|
|||
|
|
> engine scalar, and only when you spell it explicitly.**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
await brain.find({ orderBy: 'level' }) // reads entity.metadata.level — YOUR field
|
|||
|
|
await brain.find({ orderBy: 'system.createdAt' }) // reads the engine's createdAt scalar
|
|||
|
|
await brain.find({ orderBy: 'metadata.level' }) // identical to bare 'level' — explicit scope
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
There is no priority list, no "try the system field, fall back to metadata"
|
|||
|
|
behavior, and no name that resolves differently depending on what else
|
|||
|
|
happens to exist on your entities. A field called `level`, `score`,
|
|||
|
|
`createdAt`, or `type` in your own `metadata` is read as *your* field, every
|
|||
|
|
time, by its bare name.
|
|||
|
|
|
|||
|
|
## Why this rule exists
|
|||
|
|
|
|||
|
|
An internal report from a production deployment found that a user metadata
|
|||
|
|
field literally named `level` was being silently shadowed by the engine's
|
|||
|
|
own internal index layer field of the same name — every sort by `level`
|
|||
|
|
returned insertion order, with no error raised. This rule makes that class of
|
|||
|
|
bug structurally impossible: bare names belong to you, unconditionally, and
|
|||
|
|
anything that isn't yours has to be spelled out.
|
|||
|
|
|
|||
|
|
## The system scalars
|
|||
|
|
|
|||
|
|
`system.<field>` addresses exactly ten scalars on an entity — no more, no
|
|||
|
|
fewer:
|
|||
|
|
|
|||
|
|
| System field | What it is |
|
|||
|
|
|---|---|
|
|||
|
|
| `system.id` | The entity's id |
|
|||
|
|
| `system.type` | The entity's `NounType` |
|
|||
|
|
| `system.subtype` | The per-app sub-classification passed to `add()` |
|
|||
|
|
| `system.createdAt` | When the entity was created |
|
|||
|
|
| `system.updatedAt` | When the entity was last written |
|
|||
|
|
| `system.confidence` | The `confidence` param (0–1) |
|
|||
|
|
| `system.weight` | The `weight` param |
|
|||
|
|
| `system.visibility` | `'public'` / `'internal'` (see the visibility tiers in [Consistency Model](./consistency-model.md)) |
|
|||
|
|
| `system.service` | The multi-tenancy `service` tag |
|
|||
|
|
| `system.createdBy` | Who/what created the entity |
|
|||
|
|
|
|||
|
|
Relationships mirror the same eight shared scalars (`subtype`, `createdAt`,
|
|||
|
|
`updatedAt`, `confidence`, `weight`, `visibility`, `service`, `createdBy`)
|
|||
|
|
plus three of their own:
|
|||
|
|
|
|||
|
|
| System field (relationship) | What it is |
|
|||
|
|
|---|---|
|
|||
|
|
| `system.verb` | The relationship's `VerbType` |
|
|||
|
|
| `system.sourceId` | The id of the entity the relationship starts from |
|
|||
|
|
| `system.targetId` | The id of the entity the relationship points to |
|
|||
|
|
|
|||
|
|
Anything not on these two lists is not a system scalar — `system.<name>` for
|
|||
|
|
any other name refuses (see "Refusal semantics" below), even if that name
|
|||
|
|
sounds like it should be engine-owned.
|
|||
|
|
|
|||
|
|
## Invisible plumbing — never addressable, in either spelling
|
|||
|
|
|
|||
|
|
Five names are pure engine internals. They are not reachable as a bare name,
|
|||
|
|
and not reachable as `system.<name>` either — they simply have no place on
|
|||
|
|
the query surface:
|
|||
|
|
|
|||
|
|
- **`vector`** — the stored embedding. It participates in similarity search
|
|||
|
|
(`query`, `near`, vector `find()`), never in `where`/`orderBy`/`groupBy`.
|
|||
|
|
- **`connections`** — graph adjacency. Reached through `connected` and
|
|||
|
|
`brain.related()`, not through field addressing.
|
|||
|
|
- **`level`** — the internal index layer number used by the nearest-neighbor
|
|||
|
|
graph. It is pure index plumbing with no query-surface meaning at all —
|
|||
|
|
which is exactly why a user field of the same name must never be shadowed
|
|||
|
|
by it. `level` as a bare name is always yours; there is no engine-owned
|
|||
|
|
spelling of it to compete with.
|
|||
|
|
- **`data`** — your entity's content payload, not a scalar. It can be a
|
|||
|
|
string, a number, or an arbitrary object, so sorting or filtering it as a
|
|||
|
|
single comparable value would lie about its actual shape. Content is
|
|||
|
|
reached through the content/text-search APIs (`query`, `searchMode:
|
|||
|
|
'text'`), not through `where`/`orderBy`.
|
|||
|
|
- **`_rev`** — the per-entity revision counter used for optimistic
|
|||
|
|
concurrency (`ifRev`). It is a CAS token, not a queryable dimension.
|
|||
|
|
|
|||
|
|
`system.level`, `system.vector`, and `system.data` all refuse for the same
|
|||
|
|
reason: they are not in the ten-scalar system map, full stop.
|
|||
|
|
|
|||
|
|
## `metadata.<field>` — the explicit spelling of "mine"
|
|||
|
|
|
|||
|
|
Prefix any field with `metadata.` to say the same thing a bare name already
|
|||
|
|
says, spelled out. The two are interchangeable everywhere a field name is
|
|||
|
|
accepted, including `orderBy`:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
await brain.find({ where: { 'customer.tier': 'gold' } })
|
|||
|
|
await brain.find({ where: { 'metadata.customer.tier': 'gold' } }) // identical
|
|||
|
|
await brain.find({ orderBy: 'metadata.score', order: 'desc' }) // identical to orderBy: 'score'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Reach for the explicit spelling when it reads more clearly next to a
|
|||
|
|
`system.` field in the same query — for example, sorting by your own `score`
|
|||
|
|
while filtering on `system.confidence`.
|
|||
|
|
|
|||
|
|
## Refusal semantics
|
|||
|
|
|
|||
|
|
A name that resolves to neither your metadata nor a system scalar is a typed
|
|||
|
|
refusal, not a silent empty result and not a guess. Refusals name **both**
|
|||
|
|
candidates, so the fix is always in the error text:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
await brain.find({ orderBy: 'createdAt' })
|
|||
|
|
// UnresolvableFieldError: no metadata field 'createdAt' — did you mean
|
|||
|
|
// system.createdAt or metadata.createdAt?
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`UnresolvableFieldError` is exported from the package root:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { UnresolvableFieldError } from '@soulcraft/brainy'
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await brain.find({ orderBy: 'createdAt' })
|
|||
|
|
} catch (err) {
|
|||
|
|
if (err instanceof UnresolvableFieldError) {
|
|||
|
|
// err.message names both candidates — usually enough to fix the call site.
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
A handful of `find()` options are not implemented yet: `cursor`,
|
|||
|
|
`includeRelations`, and `writeOnly`. Rather than accepting them and quietly
|
|||
|
|
ignoring the option, `find()` refuses with `UnsupportedFindOptionError` —
|
|||
|
|
also exported from the package root — so a call site can never believe an
|
|||
|
|
unimplemented option took effect when it didn't.
|
|||
|
|
|
|||
|
|
## The ordering contract
|
|||
|
|
|
|||
|
|
`orderBy` behaves identically regardless of which engine (the pure-TypeScript
|
|||
|
|
path or a native accelerator) is serving the query:
|
|||
|
|
|
|||
|
|
- An entity missing the `orderBy` field, or holding `null` on it, sorts
|
|||
|
|
**LAST — in both `asc` and `desc`**. It is never treated as "smaller than
|
|||
|
|
everything" in one direction and "larger than everything" in the other; it
|
|||
|
|
is simply last, either way.
|
|||
|
|
- Rows are **never dropped** from an ordered read because they lack the
|
|||
|
|
field — a missing value changes position, never presence.
|
|||
|
|
- Ties on the `orderBy` field break by **id ascending**, regardless of the
|
|||
|
|
primary sort direction.
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// employees: [{ score: 9 }, { score: 5 }, { /* no score field */ }]
|
|||
|
|
await brain.find({ orderBy: 'score', order: 'desc' }) // [9, 5, missing] — missing is last
|
|||
|
|
await brain.find({ orderBy: 'score', order: 'asc' }) // [5, 9, missing] — missing is STILL last
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Migrating existing call sites
|
|||
|
|
|
|||
|
|
If you have call sites written before this rule shipped that rely on a bare
|
|||
|
|
system name — `orderBy: 'createdAt'`, `where: { confidence: { greaterThan:
|
|||
|
|
0.8 } }`, and similar — they now refuse instead of silently resolving to the
|
|||
|
|
engine field. The fix is always in the error: swap the bare name for
|
|||
|
|
`system.<field>` (or `metadata.<field>` if you actually meant your own field
|
|||
|
|
of that name, and it happens to share a name with a system scalar):
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// Before: bare 'createdAt' silently meant the engine's timestamp.
|
|||
|
|
await brain.find({ orderBy: 'createdAt' })
|
|||
|
|
|
|||
|
|
// After: say which one you meant.
|
|||
|
|
await brain.find({ orderBy: 'system.createdAt' }) // the engine timestamp
|
|||
|
|
await brain.find({ orderBy: 'metadata.createdAt' }) // your own field named createdAt, if you have one
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
There is no silent migration path by design — every ambiguous call site
|
|||
|
|
surfaces as a refusal naming its own fix, once, the first time it runs
|
|||
|
|
against the new rule.
|
|||
|
|
|
|||
|
|
## Where to go next
|
|||
|
|
|
|||
|
|
- [Consistency Model](./consistency-model.md) — the separate (and
|
|||
|
|
longer-standing) contract for *reserved* fields: which names may never
|
|||
|
|
appear inside a `metadata` bag at write time, distinct from this page's
|
|||
|
|
read-time addressing rule.
|