99 lines
4.5 KiB
Markdown
99 lines
4.5 KiB
Markdown
---
|
|
title: Migrating to 9.0 — your fields and system fields
|
|
slug: guides/namespace-migration
|
|
public: true
|
|
category: guides
|
|
template: guide
|
|
order: 1
|
|
description: The simple story of the 9.0 field-addressing change and the mechanical checklist for updating your call sites — every miss fails loudly with the fix in the error.
|
|
next:
|
|
- concepts/field-addressing
|
|
---
|
|
|
|
# Migrating to 9.0 — your fields and system fields
|
|
|
|
The one-sentence version: **your data's field names are now completely
|
|
yours, the engine's own fields all live behind one `system.` prefix, and
|
|
nothing in between can silently go wrong anymore.**
|
|
|
|
## What changed, simply
|
|
|
|
**1. Any field name just works.** Before 9.0 the engine quietly owned
|
|
certain names. A field called `level` could be shadowed by the engine's
|
|
internal index layer of the same name (sorts silently returned insertion
|
|
order); names like `confidence` or `subtype` were rejected inside
|
|
`metadata`; names like `content` or `id` were silently never indexed, so
|
|
filtering on them returned nothing. All of that is gone. Any name —
|
|
`level`, `confidence`, `type`, `id`, `content`, anything — is stored
|
|
exactly as written and works with every feature: filtering, sorting,
|
|
grouping, aggregation, search, and time-travel reads.
|
|
|
|
**2. The engine's fields moved behind `system.`.** The engine still keeps
|
|
its own per-record bookkeeping — creation time, type, confidence, and so
|
|
on. Those are reached one way only now: spelled out, e.g.
|
|
`system.createdAt`, `system.type`. They are just as queryable and sortable
|
|
as before. `orderBy: 'createdAt'` means *your* field named `createdAt`;
|
|
`orderBy: 'system.createdAt'` means the engine's timestamp. No guessing,
|
|
no priority rules.
|
|
|
|
**3. Storage keeps the two physically separate.** New records store your
|
|
metadata in its own nested compartment, so a user field named
|
|
`confidence` and the engine's confidence live side by side, both intact,
|
|
through restarts, index rebuilds, and `asOf()` history. Old records stay
|
|
readable forever; nothing rewrites your data.
|
|
|
|
**4. Mistakes are loud.** An ambiguous or unknown field name is a typed
|
|
error naming the fix. Unimplemented options refuse instead of being
|
|
ignored. The only forbidden name in your metadata is one literally
|
|
starting with `system.`.
|
|
|
|
## The mechanical checklist
|
|
|
|
Every missed site fails **loudly** with the correction in the error
|
|
message — nothing silently changes meaning. Sweep these patterns:
|
|
|
|
| Before (8.x) | After (9.0) |
|
|
|---|---|
|
|
| `orderBy: 'createdAt'` (meaning the engine timestamp) | `orderBy: 'system.createdAt'` |
|
|
| `where: { subtype: 'invoice' }` (the engine subtype) | `where: { 'system.subtype': 'invoice' }` |
|
|
| `where: { confidence: { greaterThan: 0.8 } }` (the engine scalar) | `where: { 'system.confidence': { greaterThan: 0.8 } }` |
|
|
| `groupBy: ['noun']` or `groupBy: ['type']` | `groupBy: ['system.type']` |
|
|
| `where: { visibility: 'internal' }` / `{ service: … }` (engine values) | `'system.visibility'` / `'system.service'` |
|
|
| `metadata: { confidence: 0.9 }` expecting a throw or a lift to the engine scalar | it is YOUR field now — set the engine scalar via the `confidence` param |
|
|
| `new Brainy({ reservedFieldPolicy: … })` | remove the option (it throws with this note) |
|
|
| `find({ cursor })` / `includeRelations` / `writeOnly` | refuse with `UnsupportedFindOptionError` — they were silently ignored before |
|
|
|
|
If a bare name in a query was genuinely *your* field all along (`orderBy:
|
|
'score'`, `where: { status: 'active' }`), **change nothing** — bare names
|
|
mean your fields, always.
|
|
|
|
## What happens at first open
|
|
|
|
Each existing database rebuilds its derived indexes once, automatically,
|
|
at the first open on 9.0 (index epoch 3 — the index keys split the two
|
|
namespaces). One-time cost, observable via `getIndexStatus()`; no manual
|
|
step, and your stored data is not modified.
|
|
|
|
## For tooling and raw-record readers
|
|
|
|
If you read raw stored records (fact-log scanners, export tooling), use
|
|
the exported shape-aware splitters — they handle both record eras:
|
|
|
|
```typescript
|
|
import { splitNounMetadataRecord } from '@soulcraft/brainy'
|
|
const { reserved, custom } = splitNounMetadataRecord(rawRecord)
|
|
// reserved = engine fields · custom = the user's bag, ANY names
|
|
```
|
|
|
|
Feature detection (never version-sniff):
|
|
|
|
```typescript
|
|
import * as brainy from '@soulcraft/brainy'
|
|
const lawActive = 'FIELD_ADDRESSING_CAPABILITY' in brainy // 'field-addressing/v1'
|
|
```
|
|
|
|
## Where to go next
|
|
|
|
- [Field addressing](../concepts/field-addressing.md) — the full contract:
|
|
the ten system scalars, the relation mirror, refusal semantics, and the
|
|
cross-engine ordering guarantees.
|