Skip to content

Make typed random-access structures opt-in (default off) in 5.1 - #1152

Merged
kriszyp merged 6 commits into
mainfrom
kris/typed-structs-default-off
Jun 9, 2026
Merged

Make typed random-access structures opt-in (default off) in 5.1#1152
kriszyp merged 6 commits into
mainfrom
kris/typed-structs-default-off

Conversation

@kriszyp

@kriszyp kriszyp commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

Builds on main's a2d0fafa7 (which gates struct-mode writes to primary DBIs for v4-downgrade compat) to make the primary-DBI default itself controllable, so typed random-access structures can be turned off by default and opted into where safe:

  • storage.randomAccessFields config (default false) — global default for primary stores.
  • @table(randomAccessFields: true) GraphQL directive — per-table override, persisted at table creation (like sealed/compression).
  • OpenDBIObject: primary randomAccessStructure now follows the config (isPrimary && RANDOM_ACCESS_FIELDS); non-primary stays off (a2d0faf's v4-downgrade behavior unchanged).

Why

Typed structures key the per-encoder dictionary on per-field value width, which on CDI produced (1) unbounded structure explosion → OOM on wide/variably-typed schemas, and (2) cross-replica dictionary divergence → decode failures. Classic shared structures are bounded and width-agnostic, so defaulting primary stores off is the conservative choice.

What to look at

  • databases.ts — two pieces: (a) the per-table override flows randomAccessFields from the table definition → primaryKeyAttribute (persisted) → dbiInit.randomAccessStructure at both primary-store open paths; (b) openIndex keeps custom-index object stores (HNSW vector graphs) in struct mode regardless of the config — their internal node shapes (numeric-keyed per-level connection arrays, quantized int8 bins) rely on struct encoding, so the table-level default-off would corrupt the graph. This is the key correctness point: verified that without it, both float and int8 vector-index tests fail.
  • graphql.ts — Boolean coercion handling a real boolean false, the string "false", and absence.
  • OpenDBIObject.ts — the isPrimary && RANDOM_ACCESS_FIELDS reconciliation.

No change to RecordEncoder.ts (main's a2d0faf already has the _writeStruct bail) and no change to the HNSW source.

Tests

  • unitTests/resources/models/randomAccessFieldsDirective.test.js — directive parse → encoder wiring (true / false / absent).
  • unitTests/resources/databases.test.jstable() directive wiring.
  • Existing recordEncoder.test.js (gating) and the full vectorIndex.test.js suite (incl. int8) pass on this branch. Full test:unit:resources: 698 passing, 0 failing.

Review

  • Codex (cross-model): re-running on the rebased diff.
  • Gemini: environmentally unavailable here (auth) — a manual pass is welcome.

🤖 Generated by Claude (Opus 4.7). Rebased onto current main; scope narrowed to config + directive + the object-store-index struct-mode guard.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request modifies RecordEncoder.ts to disable writes for typed random-access structures by default unless explicitly opted in via options.randomAccessStructure. This is achieved by overriding _writeStruct with a no-op function () => 0 to prevent potential OOM issues and decoding failures across replicas, while still preserving read capabilities. There are no review comments, and I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread resources/RecordEncoder.ts Outdated
@claude

This comment has been minimized.

Comment thread resources/RecordEncoder.ts Outdated
@kriszyp
kriszyp force-pushed the kris/typed-structs-default-off branch 2 times, most recently from d24cc8d to 29a6c97 Compare June 8, 2026 16:23
… directive)

main already gates struct-mode writes to primary DBIs (a2d0faf, for v4-downgrade
compat). This makes the primary-DBI default itself controllable:

- storage.randomAccessFields config (default off) — global default for primary stores
- @table(randomAccessFields: true) directive — per-table override, persisted at creation
  (like sealed/compression)
- OpenDBIObject: primary randomAccessStructure now follows the config
  (isPrimary && RANDOM_ACCESS_FIELDS); non-primary stays off (v4-downgrade compat unchanged)

Typed structures key on per-field value WIDTH, so wide/variably-typed schemas mint an
unbounded per-encoder dictionary (OOM) and diverge across replicas (decode failures);
defaulting them off for primary stores is the conservative choice, with opt-in where safe.

Also keep custom-index object stores (e.g. HNSW vector graphs) in struct mode regardless
of the config — their internal node shapes (numeric-keyed per-level connection arrays and
quantized bins) are fixed and rely on struct encoding, so the table-level default-off would
corrupt the graph.

Adds unit coverage for the config + directive wiring (databases.test.js,
randomAccessFieldsDirective.test.js).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kriszyp
kriszyp force-pushed the kris/typed-structs-default-off branch from 29a6c97 to 65fad22 Compare June 8, 2026 16:54
…te paths

With typed structures disabled (storage.randomAccessFields off), decoded records
are classic-encoded and frozen by freezeData. Several write paths stamped
created/updated times and the primary key onto the record in place, which throws
("Cannot assign to read only property") when the record is frozen — e.g. when a
record decoded during transaction-log replay is re-saved. This surfaced broadly in
integration tests under the new default-off.

Records are intentionally immutable (5.2 record caching relies on it), so the fix
is copy-on-mutate: shallow-copy a frozen record before stamping it — in the table
save validate callback and the source/caching resolve write.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kriszyp
kriszyp force-pushed the kris/typed-structs-default-off branch from 513db15 to e9786f6 Compare June 8, 2026 19:22
Comment thread utility/lmdb/OpenDBIObject.ts
… collision)

When typed random-access structures are off, records use classic shared
structures whose first byte can be 66 (0x42 = structure-id #2). RecordEncoder's
rocksdb metadata heuristic treats a leading 66 as a local-timestamp prefix and
strips 8 bytes, corrupting the record (decoded as null). The audit store's
getValue decodes a value that carries no on-disk timestamp prefix, so it now
passes { noMetadata: true } to skip the heuristic entirely. Surfaced by the
MQTT "can publish non-JSON" path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
kriszyp and others added 3 commits June 8, 2026 15:19
The directive-based tests stamp randomAccessStructure in databases.ts before
the store opens, bypassing the OpenDBIObject constructor's global-config
branch. Add a focused test that opens a primary/non-primary DBI without a
directive and asserts the constructor reads storage.randomAccessFields
correctly (true -> on, false -> off, non-primary -> off).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The first version of this test set storage.randomAccessFields via
envMngr.setProperty, which passed in isolation but failed in the full unit
suite (CI) — config state isn't reliably isolated across the suite, so the
constructor read a stale false. Stub envMngr.get per-test (restored in
afterEach) so the assertion is deterministic regardless of suite ordering;
callThrough keeps other config reads real and asserts the correct key is used.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Another test in the full unit suite leaves envMngr.get wrapped by sinon and
never restores it, which broke both prior versions of this test in CI: the
setProperty version read the leaked stub's value instead of the set value, and
the sinon.stub version threw "already wrapped". Override the getter via a
defineProperty save/replace/restore that delegates to whatever get currently is
(real or another test's stub) and restores the exact prior descriptor — proven
against a simulated leak locally. Adds a non-boolean ("true" string) case to
guard the strict === true comparison.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kriszyp
kriszyp marked this pull request as ready for review June 8, 2026 23:18
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@kriszyp
kriszyp merged commit a65f072 into main Jun 9, 2026
40 of 46 checks passed
@kriszyp
kriszyp deleted the kris/typed-structs-default-off branch June 9, 2026 03:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants