Guides
Docs Guides

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.

In this section

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:

import { splitNounMetadataRecord } from '@soulcraftlabs/brainy'
const { reserved, custom } = splitNounMetadataRecord(rawRecord)
// reserved = engine fields · custom = the user's bag, ANY namesTYPESCRIPT

Feature detection (never version-sniff):

import * as brainy from '@soulcraftlabs/brainy'
const lawActive = 'FIELD_ADDRESSING_CAPABILITY' in brainy // 'field-addressing/v1'TYPESCRIPT

Where to go next

  • Field addressing — the full contract: the ten system scalars, the relation mirror, refusal semantics, and the cross-engine ordering guarantees.