Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Interrupted squash cleanup

Before a legacy squash writes its new genesis, it durably records the genesis and the history prefix it is about to retire. An asynchronous (experimental) squash records the same after it publishes. If a crash, or a retry of the squash, interrupts the cleanup, the next open of the database deletes what is left: records first, then snapshot files. A later squash does the same. Engrams that are reachable from HEAD, pinned, tagged, fenced by a committing squash or written by a squash still in progress are kept. Until the next open or squash, the leftover still uses disk space. An install or restore that replaces the engram log also drops the record. The cleanup removes at most 16 entries per synchronous block and makes record deletions durable before unlinking snapshot files (#1512).

Late write admission qualification — #1504

Pipelined ordering/read-index waits now honor the remaining query deadline. REST pipelined writes (/cypher autocommit and bulk), which answer a refusal with a not_submitted receipt, are also bounded by the bookmark-wait budget. Bolt writes are not, because Bolt has no such receipt. Focused controls cover apply lag, ordering contention, expired deadlines and disabled budgets.

A legacy synchronous CALL patinadb.squash runs on the shared serial apply path. In the 4 GiB pressure soak it held that path for 1–2 s routinely and up to 15 s (#1495 soak runs). Since the batched cleanup, the longest apply entry near each squash call was 0.19 s p50, 1.12 s p99 and 3.62 s max over 651 calls (#1507, full soak 0185c2321912). Writes to every database still wait behind it, and REST writes with a short bookmark-wait budget are refused while it runs. Tracked in #1507. This bounds admission before proposal; it does not cancel an already proposed mutation or authorize replay after an ambiguous client timeout. See the write retry contract.

Scoped procedures in write mirrors — 2026-10-21

The extended direct-client smoke reproduces a real CALL/YIELD/SET failure: WASM graph reads succeed on all five nodes, but the write mirror reports the registered function as unknown. Procedure lookup used the temporary store id instead of the source dataset’s registry namespace. The candidate carries the dataset id in the existing scoped procedure context; physical/cache ids stay unchanged and lookup never searches another database. A small regression fails before the fix and passes afterward, covering embedded writes, leader resolve, pinned-transaction resolve, two same-named database registrations and refusal in an unregistered database. Nested panic restoration also passes. Broader checks, new binaries and the extended smoke/full pressure qualification remain pending. The historical smoke/full failures remain failures; no three-hour PASS yet.

Limitations

The latest source-admission increment (#1478) has completed a serial 32 GiB SF1 comparison with result caches excluded. Performance acceptance remains open: IC1 regressed in both comparison pairs, and a separate first-request IC9 diagnostic timed out before profiling. This is not a new low-memory qualification. The subsequent FOREACH/DML copy changes pass their source regression gates; their performance and memory-pressure qualification remains open.

The development correction for #1479 preserves unique projected scalar names containing a dot in ORDER BY, including WITH/RETURN, DISTINCT and bounded ordering. Batched sort-key lookup preserves the first exact scalar alias, including NULL. Ordinary property ordering retains its existing execution path. These paths have regression coverage; their runtime performance remains unqualified.

Offline patinadb import writes current datastore state without creating an Engram history baseline. Whole-import rollback atomicity, historical queries and subsequent replica recovery are not implied by successful import/reopen. CSV typed lists are supported by import; Arrow List input and lossless list export/import round trips remain unsupported. See Bulk Loading.

This chapter is the honest inventory of what patinaDB does not do, or does differently from Neo4j. Read it before committing to a production workload.

Direct storage writes and cache retention

Deferred publication remains under correctness qualification (#1483). Deterministic controls reproduced both stale decoded-cache fields and a successful Cypher result mixing two versions between physical batches of an outer write scope. The development candidate passes the cache regression controls and adds snapshot admission before read execution: an uncertified native snapshot is refused before execution with READ_PUBLICATION_IN_PROGRESS (HTTP 503, retryable; transient Bolt status), while already admitted readers keep their stable native version. The diagnostic read-snapshot reuse toggle cannot remove that admitted-read isolation.

Focused controls also cover legacy reads with memory accounting disabled and parallel workers retaining the admitted snapshot. Standalone owned snapshots, including follower explicit transactions, refuse an overlapping uncertified publication rather than pinning an intermediate version. Internal write-scope callers retain their own read-your-writes behavior. Broad source validation, including crash/reopen, disk-fault and bookmark tests, passes. A source-bound cluster smoke and the new five-hour pressure run are still pending; runtime correctness qualification is not complete. The prior fulltext oracle failure remains a failure; its exact response was not retained, so these controls do not prove its cause. A passing small diagnostic does not clear that large-run failure.

The independent Rust customer backend handles explicit-transaction admission separately: an exact server DatabaseUnavailable or BookmarkTimeout rejection during ROUTE or BEGIN may retry within the original request deadline, before any statement is sent. A late BEGIN success cannot start a write. RUN/COMMIT failures and transport loss are not automatically replayed by this handling. This does not make an HTTP 503 after a write a rollback acknowledgement; its outcome still requires readback. The cluster pressure qualification remains open.

On the leader, BEGIN waits for the database’s in-flight writes to finish applying before it pins its snapshot. That wait now ends at the transaction deadline (tx_timeout or --query-timeout) with DatabaseUnavailable and no effect, instead of spending the deadline and failing the first statement. Under sustained storage pressure such a refusal can still occur; retry the transaction.

The decoded-cache publication fix covers L1 objects/properties, L2 adjacency and edge properties for non-deferred physical commits before logical sync(). Those commits conservatively invalidate reuse across the physical database; ordinary deferred atomic writes retain dependency-based invalidation. This does not make a sequence of direct storage calls atomic. L3/L4 result stamps also carry a durable physical-mutation revision from the reader’s native snapshot. Direct mutation commits update that revision in the same redb transaction; normal deferred writes and empty repair commits do not. This adds a counter update to direct mutation commits and one memoized read per native query pin using L3/L4. The performance impact is unqualified. Entries spilled by older versions without this certificate are rejected and recomputed. This qualification covers non-deferred direct commits, not arbitrary mid-scope deferred flushes.

Experimental cyclic joins require explicit activation

PATINADB_WCOJ=on enables the RFC 0042 base-adjacency cyclic join candidate. auto is the default since 2026-10-17 (#1442); it admits only closing probes and intersections by cost, never the cyclic join, and off restores binary traversal. Admission is limited to connected fixed-edge regions with at most sixteen vertex variables and sixteen typed relations. OPTIONAL, variable-length paths, derived inputs and scans carrying ordering promises retain existing execution. Named and property-filtered relationships use binary probes within an admitted region.

Plain COUNT can accumulate without binding rows. Other consumers require a complete memory-admitted result before publication; payload credit stays with the query/result owner until release. Variable ordering uses available degree or label statistics, but cost-based admission and paired server qualification remain pending (#1442). See configuration.

Factorized DISTINCT, top-k and counts have no cost admission

PATINADB_FACTORIZE also gates the RFC 0042 B3–B5 factor consumers. It is on by default since 2026-10-17 (#1442); PATINADB_FACTORIZE=off restores the reference operators. The consumers have no cost admission: every proven shape engages, with or without the cost model. They cover only single fixed paths (no variable length or cyclic names) that split at a pivot into two independent branches:

  • ORDER BY … [SKIP] LIMIT whose sort keys each live on the pivot or on one branch;
  • RETURN DISTINCT (under ORDER BY) over the pivot plus one branch;
  • ungrouped count(*) with at most one cross-branch =/<> comparison.

Tied sort keys in the window, floating or container comparison values, memory refusal, and degenerate factors (for example a single hop from an anchored node) run the ordinary operator instead. Results are the same; the time spent before the fallback is not recovered. See query planning.

Independent path counting is narrowly admitted

PATINADB_FACTORIZE (on by default since 2026-10-17, #1442) adds grouped aggregation for a subset of independent fixed branches. With the default cost model only the per-pivot count path is cost-admitted, and only once the degree catalog has the groups; until then the flat aggregate runs. Branches that share a relationship type are admitted only when they reach the pivot through it in the same way (at most two hops deep), and then only for COUNT and COUNT(DISTINCT pivot). Floating/container grouping values and sums whose signed subtotal bounds can overflow retain reference execution. The general factor/Flatten framework is incomplete; this is not full RFC 0042 B1/B2 support. Memory refusal before output also retains reference execution. See query planning.

The agg.disjoint_path_count candidate avoids cross-product rows for eligible COUNT(*) paths. It does not implement general join aggregation, grouped counts, DISTINCT counts or shared-relationship-type branch factorization. Selection uses branch length rather than measured fanout. The original LSQB comparison at 928cd9ad completes Q1/Q6, but other query regressions remain; there is no overall performance qualification. See query planning.

The integrated development alternative may select a cheaper estimated entry inside the shorter independent factor. It retains one memory-admitted UUID/count table under the existing group cap; those extra bytes can change resource refusal. Missing statement scopes and sentinel/internal group bindings retain the original path. Errors encountered after execution begins are not retried on that path. Primary-only label counts and bounded cold probes are heuristics, particularly for secondary labels, and add planning cost. Its focused and restored tests, actual hook-bypass counterexample and lint checks passed, as did combined broad source validation. Native performance qualification remains pending.

Pure count factors may additionally retain one governed neighbor set for a fixed EXISTS endpoint (PR #1410). This adds memory beyond pair memoization and can change resource refusal. Reuse requires a completed bounded scan and live index views; overlays retain point probes. A scan can encounter malformed adjacency entries that individual point checks would not read. Source validation passes; original-query Q9 ABBA shows a benefit, while Q2 timing remains inconclusive. See the engineering LSQB ledger for measurement scope; this is no general performance guarantee.

Development join counts and APPLY streaming remain conditional

PR #1410 adds partial counting only for ungrouped COUNT(*) over proven vertex-keyed inner joins after ordinary join ordering. Scalar keys, optional matches, DISTINCT and intervening filters retain existing plans. Grouping changes retained memory and can change the point of resource refusal.

APPLY streams only under a whole-consumer snapshot-read proof. It still retains the outer input and one seed result, and it keeps cumulative output limits. Unproven effects preserve eager evaluation; a pure-read LIMIT can avoid errors in unconsumed later seeds. Correctness controls do not establish a native performance improvement. Original-query A/B measurement remains pending for this candidate; the previous Q5 regression is still open.

Buffered PROFILE memory parity: bounded validation completed

The earlier buffered Bolt PROFILE route did not carry the statement memory scope into its blocking worker. Its diagnostic output therefore cannot establish whether an ordinary read engaged a strategy that requires that scope. Instrumented PROFILE elapsed times are also not ordinary-query benchmark measurements.

The correction carries the existing scope into the read-fenced worker and retains result ownership through buffered delivery and cancellation. Memory refusal remains an error, with no unmetered retry. Its focused and restored controls, scope mutation, Clippy and formatting checks passed. The original validation run remains recorded as FAIL for an index-only ratchet issue; staging the six deleted files and new files made all 124 ratchets pass without changing Rust sources. Combined broad source validation passed; native performance validation remains pending.

The experimental early adjacency-recording gate was removed after the combined ordinary-binary comparison established no material Q2 benefit. This removes its additional doorkeeper lookup and cache-admission timing change; ordinary drain-time admission and generation checks remain. Historical validation and failed intermediate receipts are retained. The subsequent single-vertex UUID source-emission change passed focused/restored, mutation and combined broad source validation. Its ordinary-binary performance measurement remains pending.

Ordinary transient EcoVec rows used during traversal and predicate evaluation are not all charged to the statement memory governor. A row-construction counter is not an admission counter, and release of registered result owners does not prove that every transient allocation was covered by the query budget.

Transaction memory admission is in development

The combined server source wires RFC 0036 admission through Main with one cgroup-aware budget, shared work governor and inventoried native plan. Focused configuration/startup and combined source checks pass. ENV/CLI/YAML controls and read-only --check-config use the same resolver; PATINADB_CACHE_LIMIT=0 disables entry caches while preserving work admission and the native allowance. At 4bfb2d3e, the bounded Q/TX/foreground control and original 120-second default smoke pass with actual managed settings and five hard-2-GiB/no-swap nodes. The separate full pressure run passes the agreed runtime gate after independent and final review, with explicit cap overshoots and sampling gaps. Performance measurements are complete with regressions; performance acceptance remains open. See the configuration. Earlier source-qualified subsections describe their individual increments.

The later Bolt streaming-scope change has passed focused Core/operator/output controls and broad regression validation. Covered parameter and expression admission now follows the whole blocking read worker, including live and plain historical read fences. Context-held credits can retain dead intermediates until worker exit and conservatively reject a long stream before its exact live heap reaches the cap. Sort scratch and encoded frames keep their own lifetimes; frame delivery/discard can release bytes independently. Cancellation seals admission, but is not an immediate worker-join guarantee.

This scope does not cover every streaming allocation. L3/L4 RowTable hits still copy outer containers without a query owner; ordinary lazy hydrate_row still builds property containers and recursively hydrates scalar values outside that admission. The existing bounded L3 accumulator and native/cache/transport internals remain separate. The earlier 4bfb soak and prior-source performance numbers do not qualify this later change or establish the requested 10% overhead bound. See the current streaming scope.

The completed bounded comparison of that streaming scope at 08bd4971 still finds a first-read increase in every paired block; the requested maximum 10% overhead is not qualified. A separate same-binary cache-initialization control supports shifting work before the first query, while retaining the preparation cost. It uses a synthetic policy access and is not a product-performance result. The later product candidate prepares the existing empty policy sketch without recording any access or changing a setting. Disabled construction and later embedded activation remain lazy. Source/negative controls pass. The new candidate’s original query/retained measurements now complete with improved first-read medians and adverse individual blocks retained. A general 10% bound and final-source pressure qualification remain open. See the performance ledger.

The first managed-default smoke failed on temporary native-cache admission before mixed load. A correction to tag-export owner lifetimes passes source and bounded integration checks. The next managed-default smoke also fails: after two acknowledged seed batches, the third refuses on native capacity before mixed load. A follow-up counts retained tag-export bases, overlapping snapshot jobs and one foreground mirror inside the same native allowance, reducing automatic per-file caches. Its source, real-owner checks and subsequent default smoke pass. The separate full pressure run passes its agreed workload; performance acceptance remains open. A failed chunked request can retain earlier committed chunks and must not be replayed as a whole without reconciling its outcome. See the validation ledger.

A later bounded managed-default control completes its query-refusal and subsequent-progress checks, then fails because a correctly typed Bolt TX memory refusal lacks effect metadata. Its TX no-effect/progress checks and foreground case are not completed by that failed run. The correction at 4bfb2d3e passes source checks and a new actual Q/TX/foreground control: exact typed non-retryable refusal with no effect, preserved graph/HEAD/history, subsequent one-Engram progress, five-node readback and accounted-owner release. The separate default smoke passes all original faults/backend/UI oracles and seven service generations. Sampled OOM evidence is zero with explicit gaps; the small smoke shows no reclaim and is neither 10 GiB-size nor three-hour pressure evidence. Accounting, limits and execution behavior stay unchanged. Paired measurements of a15499a0 do not qualify the corrected candidate. See the boundary ledger.

The interim RFC0036 integration comparison uses small one-voter fixtures. It observes both median variability and higher tails in some cases; it is not a performance acceptance, safe-default or full-pressure qualification. The new 4bfb write matrix also finds higher unconstrained 1 KiB COMMIT p99 in all four paired blocks, both on/base and on/off; see the performance ledger. The completed 4bfb retained-tail comparison also finds higher first Bolt-read latency in every block, while its exact recovery/reopen checks pass. The complete three-hour five-node pressure run now completes 10800.334 mixed seconds after 36,880 x 64 KiB values and five allocated graph files above 10 GiB. Its final auditor verifies all three faults, eight in-mix Q/TX refusals and separate post-mix foreground refusal/release, positive timed reclaim and observed OOM0 across seven generations, retaining sampling gaps and transient values above configured MemoryMax. Final review accepts this scoped runtime qualification; there is no universal capacity/concurrency or performance PASS. The measured file growth does not establish long-term stability or compaction behavior. See the full outcome and limits.

The explicit governor’s normal-write gate checks already resolved request data against a checked 6 * counted_data_bytes + 64 Apply allowance before proposal. It includes conditional preimages and Anamnesis data and preserves existing TX/Engram boundaries. Earlier producer reservations remain necessary; the check is neither query-text prediction nor proof that state-dependent index expansion, native pages or every follower’s committed working set fits. No second Apply reservation is taken for the same data. A rejected chunk is not proposed; a chunked import retains its successful prefix and latches the first terminal proposal failure, including under ON ERROR CONTINUE. This preproposal check alone does not qualify the candidate defaults. See the scope and pending validation.

The explicit Rust native-cache API shares an aggregate capacity across actual persistent, temporary, checkpoint and simulated-disk opens. Application entry capacity is reserved in the same split. A protected native floor keeps temporary opens from consuming capacity intended for persistent graph/Raft/audit stores; both classes still obey the total. Physical replacement admits its new native cache before removing the old store and transfers that reservation into the replacement. Ordinary strict refusal preserves the old registry entry and staged file; retained readers keep the old generation charged. Explicit required replacement uses the smaller-grant policy described below with the same ownership ordering.

Startup also inventories existing eligible graph directories, missing reserved databases and the separate Raft/audit files before opening its first graph. It reserves and transfers one native lease per file. Capacity refusal precedes file initialization and returns the unused plan’s reservations. Later native/schema failures return unconsumed reservations, but transferred leases stay charged until their last native owner drops; independent snapshot Trees can outlive the outer open error. Earlier durable initialization can remain on disk. Main now selects the inventoried sizing API described below. Successful startup planning alone does not qualify replica Apply/recovery under memory caps. Cache capacities are not exact resident-memory limits; native scratch/backing and pinned pages remain separate.

Startup also transfers one reserved native cache slot for committed Fork/Squash reconstruction. Its native cache charge stays held until the last backend/read guard returns the slot; contending calls wait before history/retention/write locks. Native/Sim ownership and bounded committed-command controls pass. This does not change optional reconstruction or legacy constructors, activate defaults, solve permanent fork-target capacity or qualify all recovery. Whole-Apply cancellation ordering remains a separate obligation.

An explicit sizing API derives primary and bounded scratch requests from the same startup inventory and aggregate allowance after existing entry shares. It includes known graph/Raft/audit owners, one required reconstruction slot, retained tag-export bases, a D-mirror-plus-checkpoint snapshot wave, two shared periodic/tag jobs and one foreground mirror when otherwise free. These are finite sizing allowances, not reserved optional slots; other admitted queries can consume them. The updated formula’s source checks pass, followed by the bounded automatic-default smoke above. Arbitrary concurrent/retained generations, future database creation and other mandatory recovery owners are not qualified by that finite count. Exact REDB overrides, including zero’s 1 GiB native default, must fit or reject before installation. See the source validation and remaining scope.

The required-native increment with focused source validation allows explicit committed Create/Anamnesis/Fork targets and received snapshot/restore opens to receive a smaller configured cache, including zero, when the preferred request cannot fit. Required pending/forced repair mirrors and checkpoints likewise use only the remaining native and temporary allowance before the durable install intent clears. Ordinary opens and periodic snapshots stay strict; real storage errors and invalid accounting are not swallowed. A zero-byte lease still follows the last native owner, and old generations remain charged. Startup and exact ENV overrides stay strict: PATINADB_REDB_CACHE_LIMIT=0 continues to request 1 GiB.

Under automatic sizing a database created after startup gets no configured native page cache until the next restart (#1499): its grant may not consume the temporary inventory kept for snapshot and mirror work. Reads of such a database depend on the OS page cache; that performance cost is unmeasured. Restart, a dropped database or a smaller explicit PATINADB_REDB_CACHE_LIMIT makes capacity available.

This removes only the configured-cache obstruction on those required paths. Zero cache still allocates active/pinned/dirty pages and native metadata; in-memory repair backing is independent of the cache allowance. It is not a whole-memory or liveness guarantee. A remaining resource/storage failure during committed Apply still follows the existing failure path rather than silently skipping the entry; clients must not infer permission to replay writes. Full required-memory, pressure and performance qualification remain prerequisites for default activation. See scope and executed source validation.

The existing authenticated Admin metrics route now reports the explicitly installed work governor’s accounted pools. Disabled pool values are NaN, not observed zero. Foreground zero only describes reservations at that scrape; it does not prove all workers stopped, all allocations were admitted or RSS fell. See accounted work memory.

An explicit Linux monitor can reuse the existing process tick to fence new positive foreground and optional-background reservations after a lower effective ceiling is observed. It keeps the original startup baseline, retains existing owners and permits protected committed Apply/recovery. Unknown samples are not zero; an increase does not reopen admission. Main now selects this monitor from the same retained startup discovery and budget. Its configured two-second sampling interval is not a response-time or OOM-prevention guarantee. See the configuration.

The combined startup, metrics and monitor source checks pass, including four behavioral negatives and restored positives. A test-only native-lock witness strengthens durable-reopen checks without changing their shutdown deadline or retrying database open. It does not establish general recovery latency. No new full-pressure, default or performance qualification follows; see the increment scope.

The explicit query context covers retained table/key storage for wildcard and explicit-column projection DISTINCT. Known duplicate keys are not recopied. The explicit-column path also accounts retained result slots and still consumes all input before yielding output; duplicates still count toward its row cap. Slot credit follows the iterator through query sealing and early consumer drop. Ordinary plan UNION DISTINCT now shares the same collector while preserving arm-wise execution and its all-input row cap. Seeded/APPLY, memo row copies and both central read RETURN collectors now retain row-slot credit through their actual allocation lifetime. This is runtime allocation accounting, not query-text cost prediction. The central RowTable hydrator now admits its output containers, fresh property decoding, label arrays and scalar copies; its result owner keeps credit through client delivery and query sealing. Fresh scalar string copies can add cost, which is not yet measured. Original Row/EcoVec/value producers, Raw streaming and alternate result emitters, remaining operators, memo keys/table and top-level Cypher result merge still need coverage. Keyed and cartesian Join output now also reserve fresh row backing and recursive copied values before construction (#1468), and produce one combined row at a time. Credit remains with query/result delivery even if downstream operators have already discarded intermediate rows; this can conservatively refuse work before current live output reaches the cap. Probe key copies have a separate iterator-local owner (#1469), released when advancing or dropping the probe; they do not accumulate in delivery. Keyed build hash allocations, composite keys and bucket capacity are admitted under the shared table owner (#1470). Join input slot containers now retain their own permit through iteration or keyed build handoff (#1471). Sort input, correlated/rooted MATCH candidates and accumulators, and OPTIONAL input/extensions/results/memoized candidate buffers also retain row-slot credit (#1472). Cache copies count their overlapping capacity; residual filtering retains the original buffer’s capacity. Pre-existing row payload producers, optional memo keys/table storage and model clones still need coverage. Shared seed/import selection, APPLY/OPTIONAL merges, OPTIONAL NULL extension and selected comprehension merges now admit copied payloads (#1473). Unique owned input entries move; shared entries reserve their copy. Credit remains with query delivery even after a temporary row is discarded, so this can conservatively refuse before the live output alone reaches its cap. Within one pattern comprehension, unchanged outer bindings can now remain in one admitted row while unique local rows with the same binding layout are moved into it (#1475). Shared rows or changed layouts use the existing copy checks. The first imported payload still requires admission; this adds no cache or setting. Property reads on an Entity binding use its identity without copying unrelated hydrated properties (#1477). The existing storage lookup still determines the value; cached hydration does not override the current transaction’s value. Selected-value decoding and other uncovered producers retain their existing limitations. This change adds no setting or measured latency guarantee. UNWIND now admits selected row backing and recursive carried-value copies before appending a binding (#1478), including Retain-fused and seeded execution. Ordinary and fused outputs stream one row at a time; seeded execution retains its existing bounded collection. Copy credit stays with the enclosing query/result owner, conservatively including discarded intermediates until that owner drops. The development follow-up also admits UNWIND source-list copies and containers; source failures return before output or a FOREACH body. Non-fused Retain admits kept copies of shared rows and shrinks unique rows in place, preserving their existing backing capacity and producer credit. Refusal, result ownership and caller error propagation have regression coverage. Native property decoding and other producers remain separate gaps. No throughput or pressure-soak qualification follows from these correctness tests. The FOREACH follow-up applies the same carried-value checks to the initial body row and reserves its container through the existing Rows owner. Refusal, shadowing, entity provenance, credit lifetime and atomic rollback/reopen have regression coverage. CREATE/MERGE binding containers are covered by the additional admission described below. Scalar map-field reads in CREATE/MERGE use the existing borrowed evaluator, copying only the selected value with query admission. Function and compound property expressions now also admit temporary binding-row containers and their recursive value copies before evaluation (#1478). Copy credit follows query result delivery, including discarded intermediates. Refusal, scalar-result correctness, credit lifetime and atomic rollback/reopen have regression coverage. MERGE now borrows seed rows, admits match/output row-vector storage, and admits carried seed copies before ON MATCH. Output-vector credit follows query delivery; intermediate credits are conservatively held until delivery. Refusal, ON MATCH semantics and atomic rollback/reopen have regression coverage (#1478). CREATE/MERGE binding maps now reserve a conservative backing allowance before allocation and admit recursive seed copies. Created-row backing and mutation Apply/Union/read collectors also use admission; FOREACH CREATE shares these paths. The map allowance is checked against allocator requests on the supported toolchain. Intermediate credit is conservatively retained through query delivery, so a query can be refused before its actual live heap reaches the configured cap. Native property decoding, other model copies and OPTIONAL memo storage still have separate coverage gaps. Source regression and rollback checks pass (#1478); throughput and the final five-node memory-pressure soak remain unqualified. This adds no runtime setting or complete query-RAM guarantee.

The development query context now also admits full-sort key-slot containers, index permutations and visited markers before allocating them. A scratch refusal propagates as a resource error before row reordering. Stored vertex/edge property keys also decode under the sort owner’s allowance, with admitted lookup scratch released per batch. Property-cache clones are bypassed for fresh decode; the performance impact is still unmeasured. Existing rows, expression/scalar key producers, lowering, native/registry buffers, top-K and aggregation still need coverage. This development path adds no setting or complete query-RAM guarantee.

With the explicit development governor, both expression evaluators now share admission for argument arrays, list/map expression containers, comprehension output and generated integer lists. The existing buffer budget checks actual requested capacity before allocation and old/new overlap before growth. A typed resource failure stops the admitted producer and reaches the normal query error boundary; no write is proposed by a failed private resolve. Argument slots release after the call. Returned container credit stays with the query/result owner, conservatively including dead intermediates until that owner drops. This can reject work sooner than exact live-heap accounting. Strings, recursive value clones, entity/row payloads and remaining producer paths need separate admission; these container credits do not establish complete coverage or an RSS bound. Existing row caps, semantics and error types remain. No new setting/default is activated.

The development governor now also applies one coarse allowance before scalar string production: 6 * work_bytes + 64, with checked arithmetic. This is a counting policy, not six times measured RSS. trim variants, string reverse, left, and each split piece use actual UTF-8 input bytes. ASCII case conversion uses the same byte count; non-ASCII lower/upper conversion uses three times input bytes to allow expansion. replace counts actual non-overlapping matches (also empty patterns) and uses the larger of input and output bytes. substring and right use four times input bytes to include their existing char-array scratch. A 500-byte ordinary string therefore reserves 3064 credits, without allocating 3064 bytes merely for accounting. Split also admits its growing list slots.

The shared arithmetic executor also applies this allowance to + string concatenation, using the combined UTF-8 byte lengths of the actual values. A mixed i64/f64/bool operand adds 512 work bytes for the existing scalar formatter before the sixfold allowance. That fixed allowance is a policy, not its measured RSS. The same path serves both expression evaluators. List concatenation admits its growing output slots and moves existing elements into them, preserving nested payload storage instead of recursively cloning it. Existing source buffers still need their own admission and remain separate from the new output slots. The query/result owner retains output credit. Consumed string operands release their own credits only after new output construction finishes; operands whose data may be moved into results retain their owners. Other producers can still retain dead intermediates conservatively. No parsing-based cost prediction, default or ENV switch is added. See concatenation validation.

Covered literal, parameter and scalar-binding copies in the expression executors now share runtime admission. Reading an existing string shares its EcoString backing (or copies its inline bytes) without a new expression payload charge (#1449). Source storage remains a separate obligation; newly computed strings still require producer credit. Recursive values reuse the checked container/payload copier, including map keys. Materialized table results keep fresh admitted payload copies so their delivery owner is independent of source rows. Inline strings allocate no heap payload, so the copier checks scope admission with zero payload credit; their bytes are covered by value/container storage. Heap copies reserve their fresh payload layout. String functions retain their existing producer allowance. The scalar dispatcher borrows its arguments and admits selected/copied output instead of eagerly cloning its first argument. Consumed list/map indexing moves the selected payload. Output credits remain with the query/result owner, conservatively including dead values. This is scoped coverage; other row/AST/value copies and producers remain open. See runtime value-copy validation.

The augmented runtime rows used by both expression evaluators for local bindings now reserve their actual container capacity and copied outer payloads before allocation. Mutating shared row backing first admits a fresh copy through the same helper. Lexical shadowing and evaluation order remain unchanged. Source storage and inserted values still require producer ownership; copied row credit stays with the enclosing query/result owner, conservatively including dead values. Other row producers remain uncovered. This adds no keyword-based prediction or configuration switch. See runtime binding-row validation.

The library transformations and Unicode behavior remain intact. Credit is reserved before the call and retained with the query/result owner; dead temporary strings can stay charged until that owner drops. These estimates may reject work before its exact live heap reaches the limit. Existing value ownership, other conversions, remaining recursive clones, native memory and remaining producer paths are separate. All-scalar Unicode and real allocation-request probes support the policy for the tested toolchain and cases; allocator slack/RSS are not proven by those probes. Performance impact is unmeasured. No new knob/default is enabled.

RFC 0036 does not yet enforce complete transaction/query memory coverage across server requests. Request clones retain an attached reservation until the last data owner drops, but legacy producers and decoding still require allocation coverage. The admitted buffer primitive also covers old/new container overlap and its transfer to the shared operation owner. Completed-statement buffers can retain their TX charge without freeing process credit, while sealed query aliases cannot allocate again. Explicit Core resolve methods now admit new journal payloads and collector storage on a disposable mirror and preserve that owner on return. Oversize/overlap refusal leaves the tested live graph and HEAD unchanged, including after reopen. The five Core SI resolve paths also expose admitted capture, including both retained transaction mirrors. Capture refusal poisons a retained mirror so subsequent calls cannot read partial private writes. The returned operations keep their query charge and do not close the query’s result-delivery allowance. A Core segmented TX owner can retain those statement buffers while queries remain open; its directory and final container overlap are admitted before allocation. Only completed, explicitly sealed statements can freeze into a shared batch. Whole-TX preflight still precedes any staging write across segment boundaries. An explicit Rust Limits.memory_governor now enables admitted operation capture/retention in Bolt explicit transactions. Its statement owner moves through the blocking worker and pending result, closing admission after delivery/discard or transaction termination while live operation owners retain their charge. Owned RECORD output now has pre-allocation admission: encoding borrows Core values, uses an admitted iterative stack and reserves encoded/framed overlap plus its shared owner. PULL shares these bytes without deep-copying the record. Memory refusal aborts the unproposed transaction, including prior buffered statements. This field was opt-in at that source-validation revision; the combined Main candidate now supplies it. The assembled Bolt wire message and decoded PackStream layouts now retain admission through their actual data owners. Input and admitted RUN results share one query allowance. Incomplete-frame refusal closes the connection after aborting unproposed work; complete-frame decoder refusal retains the failed-state/RESET behavior. Control messages still share foreground capacity and may themselves be refused. RUN parameter conversion reserves maps and fresh values for explicit transactions and autocommit before construction; the admitted private Core resolve APIs also reserve runtime bindings. Workers share the binding owner; returned parameter aliases keep its credit until their enclosing result is dropped. The result wrapper does not admit its own row storage or independent clones made by its caller. Explicit admitted Bolt COMMIT also reserves its new CAS token containers, fresh values, label arrays and sorting scratch; local request/entry clones share those data owners. Old vertex/edge properties for the final CAS tokens are now decoded directly into admitted storage. Their strings, containers and iterative decoder scratch are reserved before allocation; read/decoder errors propagate instead of becoming missing properties. Refusal occurs before proposal and reports no effect. Constraint catalog construction, native page/cache/registry storage, encoded overlay read buffers, the interned label arena, fallback preimages and other write paths remain outside coverage. A large old property can therefore make COMMIT refuse even when the small update itself fitted RUN. That increment kept the wire schema and defaults unchanged; full qualification is still pending.

The common column-oriented result hydrator now admits a coarse container allowance based on actual produced cells plus separately admitted property/value payloads, retaining output credit through destruction. The explicit development governor now also connects ordinary REST/Core data reads to the same execution scope. L3/L4 QueryResult copies admit their containers and fresh payload backing, retaining credit after cache eviction. Native mutation fences protect live and plain historical execution stores, independently of effect classification; private lazy reconstruction overlays remain writable. Original row/value producers, persistent-cache/L4-decode buffers and alternate result emitters remain uncovered. This does not activate production defaults or prove complete coverage. The 4096-byte per-cell counting policy may overcharge repeated columns and released scratch; it is neither an RSS figure nor an allocation of 4096 bytes per cell.

With the explicit development governor, ordinary REST /cypher autocommit writes also use the shared Core capture and pinned CAS/OCC budgets. Oversized execution or old-value CAS preparation refuses before proposal with HTTP 400, Neo.ClientError.Statement.ResourceExhausted, retryable: false and patinadb_effect: "none". Owners retain their credits through workers and proposal. Ordinary REST reads and writes now also admit their converted parameter values/containers/keys and worker query copy, keeping their shared owner charged while any worker uses it. Write input and execution use one query allowance. The /cypher wire collector also reserves capacity before growth, including chunked bodies, and hands that query scope to execution. Axum’s existing HTTP body limit and JSON errors remain; wire refusal has the same no-effect resource response. A conservative byte/structure/escape allowance now precedes the original JSON decoder and remains with its request; the policy and overcharge are described in Configuration. Ordinary data-read response byte buffers now admit growth and retain their capacity credits through HTTP delivery and final transport aliases. Refusal discards partial output before success is sent. Ordinary governed data reads now use the fenced Core execution context and admitted QueryResult materialization described above. Remaining value producers, JSON-tree construction and completed writes/control replies retain uncovered paths. Transport frames, allocator overhead, rendered error bodies, downstream provenance copies and other REST routes remain uncovered. This does not activate production defaults. Full RFC pressure/performance qualification remains pending.

With the development governor, both Bolt autocommit modes use the existing runtime capture and RECORD budgets. Oversized captured writes or encoded results fail before proposal with patinadb_effect=none; a result refusal cannot leave that write committed. Buffered results retain their charge through partial PULL, and RESET/disconnect releases their actual owners. RESET after a successful autocommit discards delivery only; it does not undo the committed write. Pipelined execution retains the same query allowance across OCC retries and drops failed-attempt capture/result buffers before re-resolving. Its operation owner survives the conflict-check worker and proposal. Cancellation seals the query; any remaining worker data keeps its charge until actually dropped. Converted autocommit parameters share their existing immutable owner across workers and OCC attempts. Expansion refusal precedes execution/proposal and reports no effect. Streaming reads keep admitted input on their blocking worker; the pending statement closes admission on delivery/RESET/disconnect, while live worker data remains charged until dropped. Streamed RECORDs now use the same admitted encoder: queued and in-flight frames retain their byte credits until sent or discarded. A mid-stream refusal returns FAILURE after any valid prefix, never a successful truncated result/bookmark. In an admitted explicit transaction, a failed historical stream also aborts its unproposed buffered writes. RESET can then accept a small read. Idle consumers can exhaust the byte budget before the 256-row channel fills. Core executor copies/rows, result metadata, fixed channel storage and native transport/cache buffers remain outside this accounting. Both autocommit write modes also use the admitted final CAS capture described above, from the same snapshot as resolve. Old-property decode or token-budget refusal propagates before proposal; admitted callers cannot fall back to unmetered live capture. Operation and CAS owners remain charged through retries and proposal clones. Earlier constraint catalog allocations, provenance and other clients still need integration. This does not change the pipeline default or activate the proposed memory settings. With the development governor, OCC now uses admitted hash sets for entity, property and replaced-label identities. This covers the candidate, each intervening Engram’s structural marks, and pinned CAS construction in both Bolt write modes. A pipelined candidate keeps its table credits while its in-flight registry entry or a conflict-check worker still owns it. Exhaustion is a pre-proposal resource error with no effect, not a conflict retry or optional-CAS fallback. A small write can therefore refuse while checking a large unrelated commit since BEGIN; retrying a new transaction after RESET starts from a fresh base. Retained constraint maps, label arrays, copied values, prefilter sets and additive UNIQUE tuples now use that allowance too. They keep their credits across history pages; replacing a map value releases its old payload only after admitting the new one. This keeps the existing final-value and additive-UNIQUE semantics. Emitted keys and their scratch tables/values are now admitted too; their owners keep credit after the accumulator is dropped, including while waiting in the pipeline. Composite-key variants are emitted individually with the existing expansion cap and numeric equality rules. Governed store-property reads for these checks now decode under the same allowance, retaining the source credit through key construction. Missing properties remain distinct from memory/read errors. Buffered final values are borrowed; explicit deletes do not read the live value again. Live and pinned read views retain their original boundaries. Decoded label IDs and label-result arrays in OCC and pinned CAS capture now reserve before allocation too. Missing vertices are distinct from read or memory failures. CAS retains the same admitted array after removing its primary label; membership/order semantics are unchanged. The retained list of intervening Engram IDs now admits capacity growth before allocation too, including temporary old/new-buffer overlap. Metadata strings are borrowed during the walk. Many small foreign commits can therefore exhaust the allowance even if each individual body is small; this returns a resource error before proposal, not a conflict retry. Governed OCC now also decodes each stored body directly into admitted operation/value storage. RAW bytes and metadata strings are borrowed; decompressed ZSTD output remains charged while decoding. Each body owner drops after its fold; the shared constraint context survives across bodies. A refusal remains a resource error before proposal. The native ZSTD context/window, tree/overlay/cache buffers, interned label arena and catalog source data still need separate admission. No additional toggle or automatic default is activated. These development controls are not a full memory-pressure qualification or a measurement of the PR’s performance impact. Handshake/dispatch, provenance metadata, streaming parameter copies, per-evaluation value copies, mirror/index storage, Core result tables and operator state, result metadata, TLS/WebSocket internal buffers and native scratch remain uncovered; charging a decoded input does not charge its later copies. These partial Core entry points reject unsupported DDL/chunked commits before execution. They use fresh string backing and add copy/CPU work; an admitted allowance does not promise success from the actual allocator. Legacy APIs stay unmetered. Existing operation-count, wire-size and per-file cache limits still have distinct scopes. Entry sizing counts actual Postcard/JSON bytes without retaining complete encoded bodies; it does not bound the already constructed request or later transport buffers. An explicit development governor also admits outbound AppendEntries/Vote byte chunks through the shared protected Apply pool. It preserves the JSON/Postcard payloads and existing RPC deadlines. Credits follow queued/in-flight byte owners; serializer failure is a transport error, not successful truncated EOF. A full foreground pool does not consume this reserve. This does not account source-entry decoding, responses, native HTTP/TLS buffers, snapshot traffic or all committed apply/replay memory. Protected-pool competition can delay replication through transport retries; no full progress/envelope guarantee follows from this adapter. Explicitly governed append receivers also reserve their contiguous wire buffers before growth. The original actual-byte cap still applies to chunked bodies. A buffer refusal is a retryable peer 503 before Raft handoff, not a local apply rejection or a client no-effect promise. Vote ingress is unchanged; native HTTP frames and decoded entries remain outside this buffer counter. The full supported entry/progress envelope is still required before activation. The separate operation decoder now admits OperationBatch arrays and recursively owned values as they are constructed, retaining credits through the last batch clone. It uses the same peer503 refusal before Raft handoff. Entry directories, metadata/CAS fields, interned labels, parser scratch and native buffers remain uncovered. These operation credits do not cover committed apply/replay decoding, and an arbitrary stored entry is not yet proven to fit the Apply pool. Retry is not a substitute for that proof. The development governor also limits encoded replication log-read batches before decoding the entire requested backlog. Each entry stays intact, including a first entry larger than the target. This does not reserve decoded heap or constrain the sum of concurrent readers, and complete apply/replay reads remain uncovered. patinadb_raft_replication_read_limited_total counts batches stopped early at this target; it is not a memory-byte or rejection counter. Legacy reads are unchanged. Current-format forced-install boot recovery verifies its full retained tail before restore, then replays entries individually with the original group durability barrier. A temporary hash file detects source changes before each entry is applied; the durable tail format is unchanged. This removes whole-tail decoded retention from that path, not an individual entry’s memory requirement. Live forced installs also copy the native log range one entry at a time to the same durable tail before restore, then use this replay path. Their pinned native read snapshot and copy/verification I/O still need qualification. Legacy v1 boot recovery now uses an anonymous copied tail and the same verifier, leaving its descriptor unchanged; another boot still requires the original live log. This increment is validated in its scoped controls. It adds temporary disk allocation and I/O, including sync, which need complete recovery-to-ready measurement. A bounded runtime Apply diagnostic confirms another open protected-path gap: OpenRaft’s replication entry cap does not bound full committed-range decoding or multiple decoded batches queued behind a held consumer. After release all checked values apply correctly; this is a memory-scheduling gap, not observed data loss. The diagnosis alone did not qualify a scheduler fix or default activation. See Apply queue diagnosis.

The bounded Apply integration now selects the reviewed OpenRaft source. With the explicit development governor, the serial worker loads at most 64 whole entries and respects the existing encoded-byte reader target; an oversized first stored entry still progresses intact. Pending ranges coalesce without crossing snapshot commands, and source logs survive until durable Apply/install permits purge. These are scheduling bounds, not an individual entry’s decoded-memory or native-page bound. Focused integration and Raft/Sim/crash controls pass. The subsequent Bolt boundary correction resolves the earlier fixture failures with fixed test allowances and additional small-cap refusal controls. Complete resource qualification, defaults and the new pressure soak remain open. No ENV toggle is added by this integration.

Raft log append now serializes entries one at a time inside the same atomic native write transaction. Each encoded buffer is released before generating the next; success is acknowledged only after the complete durable commit. Generation or storage failure cannot publish a partial generated batch. This removes the whole-batch encoded staging buffer; input entries, individual encoded values, native transaction pages and complete protected Apply ownership remain separate obligations. No new setting or default is enabled. Runtime cost still needs measurement. See atomic append validation.

Ordinary boot under the explicit development governor now preflights each original 64-entry batch and streams its entries through the unchanged apply body/barriers before Raft construction. It retains no whole decoded batch or unused response vector. This adds a second decode pass; its startup impact is unmeasured. The fault-injection build retains its existing asynchronous seam. Individual-entry expansion, non-Apply complete log readers and complete protected replay capacity remain open; final performance is not qualified. The ordinary /cypher request decoder uses a conservative pre-parse allowance under the explicit development governor. Its bytes/structure/escape policy can reject requests earlier than their final retained size alone would require; quoted punctuation and ignored fields are included. Credit follows the decoded request, but it is an estimate rather than allocator/RSS telemetry. Native frames, error response rendering and subsequent ungoverned copies still need coverage. The proposed new memory settings are not server configuration options yet. Full input, execution, commit/result and native-cache accounting, protected replay capacity and pressure qualification are required before claiming a managed node envelope.

Mixed workloads under severe RAM pressure

Snapshot restore buffering and disposable native caches are bounded separately; neither is a total-process RAM guarantee. Native Raft log I/O runs on blocking workers, but foreground commits still wait for durable storage. Concurrent snapshot/history work can compete for storage bandwidth and exceed request deadlines even without an OOM. The original five-node, three-hour mixed-workload profile passed with at least 10 GiB allocated graph files and hard 2 GiB memory per node, including the original faults and OOM checks (#1409). It used configured background pacing, synthetic data and explicit backend reconnect after leader failure. It is not a general latency, 24-hour or transparent-failover guarantee. Timeouts and durability requirements remain unchanged. Raft log purge durably records a monotonic logical floor before acknowledging completion. Readers exclude that prefix even if physical records remain after a crash. Native cleanup resumes on startup reads and reclaims at most64 integer positions per durable transaction, sharing the ordered write lane with new appends and votes. It stops between transactions on shutdown; cleanup errors remain visible to subsequent storage operations. Server shutdown joins the current cleanup transaction; embedded callers must stop reclamation before immediately reopening the same file in the same process. Truncate still completes its bounded physical deletion before returning. These integer windows avoid a full key pre-scan, but the operation ceilings do not bound native I/O latency or dirty pages. Snapshot and log-cleanup contention remains workload-dependent. Physical checkpoint copying and package writes observe process shutdown and abandon the incomplete candidate. Private native checkpoint destinations and historical tag-export scratch files also check shutdown inside redb’s file I/O, including commits; cancellation fails and discards the private artifact. Live databases and ordinary restore retain their normal durable I/O. Successful snapshot publication still requires the original sync barriers. These checks do not preempt an already-running filesystem call or promise a fixed shutdown latency under storage pressure. Optional checkpoint copy pacing limits copied input bytes, not physical I/O or historical-tag work. It trades checkpoint completion time for lower copy pressure; the successful configured profile does not establish a foreground-latency guarantee. Historical tag-base fill pacing separately limits decoded direct-fill inputs. Other tag phases and native disk amplification remain outside the rate, so neither control bounds total I/O. Checkpoint package pacing separately bounds the byte-copy phase of completed native files. Final durability barriers and other work remain outside it.

The additional private snapshot native-write budget shares admission across checkpoint and tag-export files and includes native page rewrites. This closes a scope gap in input-only pacing; it does not bound physical filesystem traffic. The combined profile passed source-bound qualification; other rates and workloads remain unqualified.

Periodic asynchronous history snapshots use a shared decoded-record scan budget. Pacing occurs before the next record pull, not merely on compressed output. Foreground exports and explicit tag jobs retain their policy. This can prolong read-snapshot pins and copy-on-write disk retention; it is not a physical I/O limit or proof of foreground availability for arbitrary workloads.

Startup now prepares each opened graph with an empty durable commit before Raft and boot snapshot views start. This can process native freed-page work before the first replicated write, but shifts that I/O into startup and does not give a fixed startup or request-latency bound. Errors prevent admission. The completed three-hour profile retained the original memory, deadline and failure-injection conditions. Its physical file growth required separate review: owned-copy allocation tests demonstrated reusable capacity, not a bound on long-term growth. See disk reclamation.

Property layout boundary (#1399)

Schema 7 rejects older schema-6 stores before interpreting their property bytes; recreate/reimport data for this format change. The inline/external split uses a fixed 256-byte serialized-value threshold and does not intern values across owners or remove complete secondary-index keys. A query that needs wide values still reads those values. Warm hydration/point and write costs can differ, so this is not a universal latency improvement or a total-RAM guarantee. Existing query operators remain; there is no automatically selected property-column scan. See property storage for EXPLAIN/PROFILE and snapshot semantics.

Primary-scan file advice (#1399)

The Linux policy requires allocated database size greater than detected RAM at open. It is not dynamically reselected after growth or memory-limit changes. Repeated scans can benefit from reduced read-ahead and cache displacement, but the measured cold first pass can be slower. The database-size rule does not predict a query’s working set. PATINADB_PRIMARY_POINT_IO=off retains native I/O after reopening for workload-specific comparison. PROFILE’s query-thread backend-request bytes are not physical storage traffic.

Wide value-index storage (#1399)

Secondary payloads are empty, but full encoded values remain in ordered leaf keys. Native redb branch-separator shortening cannot generally remove long shared prefixes or repeated leaf values; this is not string interning. A more compact index layout can also increase physical I/O for wide primary scans under tight memory. The final prefix/multimap/dictionary controls also found selective-read and scan counterexamples; no new compact-key mode was enabled. Full-key storage remains the supported representation. See wide value-index keys.

Filtered-read preparation (#1399)

Residual string candidates and supported COUNT group values use bounded advisory preparation. Value-sorted prefixes are not representative samples: match-count extrapolation requires a uniform encoded value column and already cached population. Unknown or stale metadata may retain a less efficient path; runtime limits remain authoritative. This is not a general seek-versus-scan optimizer, a physical I/O/RSS guarantee, or a change to wide-value index layout. See filtered preparation for EXPLAIN/PROFILE scope and the conservative thresholds.

Covering reads under memory pressure (#1399)

The integer-property covering input is automatically selected within conservative admission guards. It supports eligible single-label aggregates, restores UUID encounter order and keeps the existing evaluator. Selective patterns, wide/mixed/float values, secondary labels, pending indexes and unproved shapes retain their existing access. This does not make every read index-only or choose an optimal plan for every data distribution.

Its run buffer, merge fan-in and logical scratch are bounded; existing aggregate group/DISTINCT state, file cache, backend cache and concurrent queries are separate. A scratch budget is not a total RAM limit. Estimated over-budget work declines before preparation; underestimated scratch and scratch-preparation I/O failures fall back before rows are emitted. Index errors, cancellation and errors after covering execution begins still propagate. Parallel-worker configuration retains existing aggregate admission; the policy is not a calibrated comparison of all paths. Synthetic qualification demonstrates a narrow-property benefit, not universal non-regression, customer or TB-scale qualification. See Query Planning and Configuration for the diagnostic off switch and EXPLAIN/observed PROFILE diagnostics. Alternate long-value layouts remain investigation only.

Offline restore followed by cluster expansion

Older servers can let a new replica reach the causal log index without the offline-restored graph baseline (#1325). The development correction requires single-voter bootstrap and durable snapshot/prefix retirement before expansion. Same-host SATA recovery passed at 64/4,096 tickets; independent-host and 1M multi-node recovery remain unqualified. Old bookmarks and write-token receipts do not survive portable recovery. See offline restore.

Bulk constraints and grouped ordering: post-0.21.0 correction

Released0.21.0 bulk-upsert resolvers can bypass schema constraints (#1320). The development correction shares Cypher’s constraint validation and keeps graph-chunk entities complete. Until using that correction, do not treat bulk request success as proof of constraint validity; ordinary Cypher writes retain their separate enforcement. See bulk upsert for atomic, chunked and ambiguous-response recovery boundaries.

Grouped queries that rename a field but order by its original expression can return encounter order instead of sorted groups (#1321), affecting pagination. The follow-up rebinds those keys in RETURN/WITH; on the affected revision, ordering by the projected grouping alias avoids the reproduced shape.

The combined development candidate passes a two-hour five-process workflow test with4096 generated tickets,17266 correct reads and359 measured batch/fence operations (#1261). Learner restart, leader replacement and lost-acknowledgement reconciliation pass;16 connection failures occur only during injected node outages. This is small-workflow evidence, not large-ingest or physical-WAN qualification. See repository docs/batch-sync-qualification-1261.md. Neither a completed test nor this documentation publishes the code correction into the immutable0.21.0 release.

Filtered aggregation and ordered pages

The development follow-up after 0.20.0 evaluates eligible single-string entry predicates and bounded string count(*) group keys from existing property indexes. Unsupported types, secondary-label scans, entity consumers and preparation-budget overflow use ordinary execution. Long ordered-edge scans can prepare bounded predicate membership and adjacent-hop target candidates; survivors still undergo ordinary matching, validation and ordering. A small result limit does not generally bound work when most candidates fail a filter.

Parallel aggregates retain the filtered entry, pinned parameters and covered input within each UUID range. Overestimated anchor counts can be repartitioned against the counted pinned label. Query threads still default to one; the qualification explicitly configures two, not an automatic or globally bounded two-worker admission policy. No request deadline or access filter is relaxed.

The final candidate passes twelve isolated cold starts and twenty-four correct responses, followed by a strict six-hour mixed graph/search workload with 113,327 successful timed HTTP attempts (#1261). HBASE author counts take 4.38–5.27 seconds across four isolated cold observations. Mixed author-comment pages still reach 5.42 seconds, and author-count p99 is 2.69 seconds across all projects. This is an availability/correctness qualification of the recorded workload, not uniformly subsecond analytics or an idle-relative latency SLO.

The fixture has 334,770 source tickets, three voters and two delayed nonvoting readers on one SSD host; this is not 1M/60M, real WAN, crash-injection or hard RAM-limit qualification. Evidence, artifacts, resource reserves and remaining risks are in repository docs/cold-query-qualification-1261.md; prior failed and interrupted candidates remain in docs/failed-jira-queries-1261.md.

These changes are not included in the immutable 0.20.0 release. Its earlier pagination soak remains separate evidence. Review and publication of this development increment do not retroactively change that released artifact.

Historical workload qualification

The local 1M-ticket history/maintenance qualification on 2026-09-13 was incomplete (#1261). Its run7 fixes passed the strengthened AS OF matrix, including actual historical comment bodies and row multiplicities: cold Q9 is 23.9–24.4 seconds, with mixed p90 at most 1.75 ms. Cold means a restarted process, not a flushed OS cache. All warm/mixed cells meet the 300-ms p90 gate. Tag snapshot jobs become ready under load. The measured legacy squash timed out at the operator HTTP client; that run did not qualify post-squash PITR export/restore. A request-only background wrapper would not remove the large reconstruction from replicated apply and is not implemented as a substitute. Cooperative query deadlines are not hard real-time I/O interruption guarantees. These historical failures are not evidence of backup corruption; later archive qualification does not turn the synchronous legacy endpoint into an async job. Full results and conditions: repository docs/history-read-maintenance-1261-2026-09-13.md.

The subsequent 2026-09-14 async-squash/backup drill (#1261) completed the squash and its history checks, but JSON PITR export exceeded its configured 32-GiB budget; restore was not reached. The new streaming archive and offline admin tool avoid a default total-archive cap and HTTP restore timeout. The subsequent local 1M archive/admin qualification passes, including the September16 repeat described below. Chunking does not guarantee constant total restore RAM: index rebuilding and existing graph-install bookkeeping still need memory and disk headroom.

The September16 patched-candidate repeat passes local HEAD/PITR archive restore after async squash under load (#1261). It verifies current graph/indexes, pinned historical values and refusal/non-publication for corrupted, truncated, over-budget and interrupted restores. It covers two pinned tags and64 update commits, not months of history or an independent backup host. All4,800 mixed reads succeed, with maximum query p90 at177.397ms; the old3× relative policy still warns for nine queries. The approved absolute300ms policy passes.

The repeated historical matrix passes every warm/mixed cell (maximum mixed p90 at220.765ms), but process-cold reads still take11.139–46.334 seconds; comment reads take22.398–22.478 seconds. This is not an interactive cold-history latency guarantee, and no OS cache flush was used. Keep appropriate client timeouts and qualify the actual customer’s first-touch history workflow. Conditions and source-bound evidence: repository docs/pilot-qualification-1261-2026-09-16.md. This locally tested patch still requires its normal PR/CI; the report is not general production authorization.

Learner onboarding

Portable archive version 1 omitted single-property index registrations and named index mappings (#1327). Compound definitions already travel in graph records. The corrected version 2 includes the missing metadata; older archives still restore, but require those declarations to be reapplied from schema migrations. See archive compatibility. The fresh 1M multi-node SATA recovery qualification is not complete: its schema oracle exposed this defect.

Explicit historical diff jobs avoid holding an HTTP request open, but are node-local and still materialize comparison state in RAM. They have admission, accounting, execution and result-size limits; queued/running work fails on restart. This is not a generic asynchronous Cypher queue.

Raft-server tag snapshots now run through the maintenance queue. CREATE TAG acknowledges the durable tag/pin, not snapshot completion. Waiting tag jobs do not retain native read versions (#1485); active snapshot builds and queries still can delay page reuse. Historical preparation can require replay. The post-fix five-hour pressure qualification remains open. SHOW JOBS is local and admin-only, without YIELD/WHERE or a cluster-wide aggregate. Asynchronous backup jobs are node-local full-registry jobs, not replicated or automatically failed over; interrupted running jobs fail on restart. Experimental replicated squash jobs are available through an explicitly enabled REST endpoint; legacy CALL patinadb.squash and embedded synchronous tagging remain synchronous. The async endpoint handles one bounded prefix, not an unlimited retention target. Its local 1M post-squash archive/PITR gate passes; larger histories and independent-host restore remain unqualified.

The recorded 0.15.0 local 1M-ticket simultaneous two-learner test failed its original relative gate (#1261), despite physical installs completing. The subsequently approved absolute 300 ms p90 policy does not rewrite that artifact. See release notes for values, policy and scope. A manual version is not production qualification.

Physical checkpoints include state and indexes, not the sender’s full history. The learner can catch up its current state while RFC-0025 history backfill and local anchor preparation continue. Current-state readiness is not full historical readiness or permission to become a leader. Interrupted installation can resume; completed checkpoint artifacts and pending anchor sources survive restart. For physical v5, a durable install intent replays a crash-interrupted database replacement before Raft starts. An unrecoverable intent fails boot rather than exposing partially replaced graphs; these guarantees do not extend the older logical formats’ crash contract. Forced graph repair may require rebuilding a newer cached checkpoint whose body predates the repair. The post-0.13.0 audit update reconciles receiver-only databases during physical installation: a database dropped before the snapshot cut must not survive on a returning learner. That reconciliation is part of durable install-intent replay (#1271). History backfill uses the same configured private-CA trust as Raft transport (#1273); this does not disable certificate checks or remove the separate snapshot-derived historical-base limitation. Budget disk for the wire envelope, staged database and background history work. The large local fixture measured 180.513 wire MB/s for a prepared checkpoint join, but only 48.855 allocated-source MB/s including checkpoint creation (#1251, 2026-09-06, loopback on one ext4 NVMe host, 60,030,350 vertices). Thus cold onboarding >=100 MB/s remains unmet. This is not measured WAN throughput.

The separate 1M-ticket logged-fixture SIGKILL drill passes retained-log and purged-snapshot plus newer-log recovery (#1261); it does not supersede the larger-fixture cold-onboarding limit above. During recovery, causal reads may return BookmarkTimeout, and snapshot installation can require a fresh authenticated Bolt connection. See First Customer. History status alone does not prove correct full-scale historical query results. Raw unlogged imports require an explicit history base; the current delta-only backfill does not transport snapshot-derived bases. Actual backend integration, WAN faults, off-host restore and larger-scale qualification remain open. The local regional-routing fault gate is not physical WAN evidence. Regional routing requires upgraded endpoints and compatible/strict scoped bookmarks. Readiness profiles do not replace receiver-side causal waiting. Binary downgrade after causal-domain initialization is unsupported; raw writable clones must not be exposed as independent clusters retaining the same causal identity.

Query language

For the clause/function inventory and exact unsupported forms, see the openCypher Compatibility Reference. It also documents accepted-but-partial subqueries and scalar semantic differences; TCK totals are not the current language contract.

In particular, dynamic labels/types (:$(expression)), USING INDEX / USING INDEX SEEK, and scoped CALL (variable) { … } / CALL () { … } are unsupported. Use static labels/types, automatic index selection and CALL { WITH variable … } respectively. Static labels/types are a deliberate design choice: group heterogeneous input client-side into static query templates (see the reference for index-planning rationale and safe parameterization). Backtick-quoted names follow openCypher since #1500: `K` is K and a doubled backtick is one literal backtick. Earlier builds stored a quoted label or relationship type created by CREATE/MERGE/SET with its backticks (CREATE (:`K`) stored `K`) and rejected doubled backticks; data written that way keeps the backtick-wrapped name and must be recreated. A quoted name containing a . still cannot be a property-path segment. CALL db.awaitIndexes(...) is supported since 0.16.0 as a local readiness check, not a replication barrier. Likewise, vector.similarity.cosine(...) is supported as a scalar expression with normalized scores; see the reference for executable examples and argument boundaries.

Derived planner/population metadata is a bounded in-memory cache after the post-0.13.0 audit update (#1270). Cache misses do not commit the live database; reopening starts these derived caches cold. Historical throughput figures are not a new performance qualification of this cache change.

  • The 2026-09-09 openCypher TCK inventory has 3755 / 3897 scenarios passing (#858 inventory, source 6fb6d521): 113 executed failures and 29 scenarios that never ran because of one pinned Gherkin parser limitation. Those 29 count as not passing. The runner is informational and exits successfully despite scenario failures. Failures include procedure validation, graph/path edge cases, grouping/ordering, lists/quantifiers, literals and temporal behavior. They are not all harmless missing error codes. See the compatibility reference for the supported forms rather than the old approximate failure-cluster counts.

    OPTIONAL MATCH … WHERE was closed (2026-07-08): a per-clause WHERE on an OPTIONAL MATCH now filters the optional side before the left-join null-extension (a non-matching predicate yields NULLs, not a dropped row), and a chained OPTIONAL MATCH binds variables introduced by an earlier optional. A negative sub-second-only ISO duration (P1DT-0.001S) also round-trips correctly now.

    MERGE ON CREATE / ON MATCH is supported: SET x = <entity>, SET x += <map/entity> (nodes + relationships), SET x:Label, null-in-map removal, and combined ON CREATE+ON MATCH all work, plus MERGE matches-or- creates per row and undirected MERGE matches either direction.

    Outdated grouping warning removed in the #1261 audit: projected aggregate sort keys are rewritten to their output columns before limiting. The existing test_group_by_order_by_raw_aggregate_then_limit_is_deterministic regression passes in the 2026-09-06 core suite. This verifies that specific former bug, not every aggregate-ordering edge case or a new overall TCK pass rate.

  • User-defined procedures exist, but only as read-only, sandboxed WASM modules. CREATE FUNCTION <name> LANGUAGE wasm FROM 'file://…' EXPORT '…' registers a compiled WebAssembly module, callable as CALL wasm.<name>(...); it cannot write to the graph, and has no filesystem/network/clock/RNG access (deterministic-by-construction, so it’s safe to replicate). It is not a scalar UDF — it can’t be used inline in an expression like RETURN f(n.x). See WASM Procedures. This is a different mechanism from the TCK’s own ad-hoc test-fixture procedures (there exists a procedure …), which the TCK harness now implements directly against the same CALL dispatch; procedure argument/validation gaps remain in the current TCK run.

  • Constraints: all four Neo4j kinds — uniqueness, existence (NOT NULL), node-key/relationship-key, and property-type — are supported on both nodes and relationships. CREATE CONSTRAINT [name] [IF NOT EXISTS] FOR (n:Label) REQUIRE n.prop IS UNIQUE (plus the Neo4j-4 ON (n:Label) ASSERT … form and the shorthand ON :Label(prop)), REQUIRE n.prop IS NOT NULL (existence), REQUIRE (n.p1, n.p2) IS NODE KEY (composite unique + existence over the key), REQUIRE n.prop IS :: <TYPE> / IS TYPED <TYPE> (property-type), and the relationship forms FOR ()-[r:TYPE]-() REQUIRE r.prop IS NOT NULL | IS :: <TYPE> | IS UNIQUE and REQUIRE (r.p1, r.p2) IS [REL|RELATIONSHIP] KEY are all supported, along with DROP CONSTRAINT name [IF EXISTS] and SHOW CONSTRAINTS; all replicate across a cluster and are carried in Raft snapshots + portable backups. Enforcement is at write time on CREATE, MERGE, and SET/REMOVE for nodes and on edge create/SET/REMOVE for relationships (a UNIQUE NULL is exempt; existence/key rejects a missing/null required property, including a SET n.p = null or REMOVE n.p; property-type rejects a present wrong-typed value but allows a missing/null one); CREATE CONSTRAINT refuses to run if the existing data already violates the constraint. FOREACH-body mutations are enforced too (GH #575, fixed): SET (per-property and map-form +=/=) and REMOVE inside a FOREACH (… | …) block route through the same enforcing core as a top-level SET/REMOVE, so a FOREACH write can no longer bypass a declared UNIQUE / existence / node-key / property-type constraint. A FOREACH-body CREATE pattern property that references the loop variable now resolves correctly (GH #578, fixed): FOREACH (i IN [1,2,3] | CREATE (:Tag {v: i})) creates nodes with v:1/v:2/v:3, not v: null — this was a separate, pre-existing data-correctness bug found while testing #575 (apply_foreach‘s CREATE-path resolver only threaded vertex bindings, never the scalar loop variable, into the pattern-property resolver); toInteger(i) and other function-arg positions resolve it too. (A loop variable nested inside another map literal as a pattern property — {m: {inner: i}} — used to be unsupported for the same reason plain arithmetic/string-concatenation in a pattern property was (GH #863, fixed): CREATE (n {m: {inner: i}}) now resolves i correctly, same as a top-level {v: i}.) MERGE, DELETE/DETACH DELETE, and label mutations inside a FOREACH body now execute (GH #1229, fixed): the grammar originally accepted them, but the executor silently dropped them (the original GH #579 bug — a FOREACH (i IN [1] | MERGE (:X)) returned success and wrote nothing; #579 made them a loud parse error as a stop-gap). A FOREACH body is now a source-ordered pipeline of the same updating clauses a top-level body accepts — CREATE, MERGE (match-or- create per loop item, ON CREATE/ON MATCH branches included), SET (including SET n:Label), REMOVE (including REMOVE n:Label; primary-label removal stays the documented no-op), DELETE and DETACH DELETE — each routed through the same constraint-enforcing core as its top-level counterpart, so no accepted body form is ever silently skipped. Each body clause sees the bindings the clauses before it produced in the same iteration (FOREACH (i IN [1] | MERGE (n:X {v: i}) SET n.seen = i)), and later loop items see earlier items’ writes. The loop variable is visible to every body clause, shadows a same-named outer binding inside the body, and neither it nor body-introduced bindings escape the clause (a trailing RETURN i is UndefinedVariable). Entity-valued list items — collect(n)/collect(r), [r]/nodes(p)/relationships(p) entity elements — are rehydrated to real node/relationship bindings, so label and property writes and DELETE work over collected entities. UUID scalar values (including id(n) and collect(id(n))) remain scalar data even when they name an existing node; they are not mutation targets (#1346). collect(n) now preserves the same entity-valued elements as [n], rather than exposing scalar UUID elements. Entity-valued CASE and coalesce expressions retain that identity too. Nodes, relationships and paths cannot be stored as properties, including inside lists/maps; use id(n) explicitly to store a scalar identifier. Entity values in lists/maps use native Bolt Node/Relationship structures (including labels and properties); genuine paths retain their Path type even when they contain only one node. UUID scalar values never resolve graph properties implicitly. JSON entity elements now render as objects with type, id, labels (nodes) or endpoints/type (relationships), and properties, instead of the former <path:1 elements> string. Actual paths retain their existing JSON display. Row-table scalar containers use the same explicit entity type; WASM Node/Relationship wire tags are unchanged. A body DELETE target is validated per item: an entity deletes, a null is a no-op (top-level semantics), any other scalar raises InvalidDelete (never a silent no-op). A nested FOREACH is still unsupported — the grammar has no nested body clause. Node-key and relationship-key uniqueness are each served by an index (an automatically registered backing compound index for node-key; the existing edge-property value index for relationship-key/uniqueness) — no separate index to create. Not supported: LIST<T> element-type refinement (bare LIST only) — whether that stays a hard “not supported” or becomes a follow-on is the open product question in GH #1232 — and bare composite-UNIQUE without existence. See Constraints. (Index DDL — CREATE INDEX ON :Label(prop) and compound :Label(p1, p2) — is supported and replicates across a cluster; see the Data Model.)

  • No integer element id, and a relationship’s id is its endpoint triple. id(n) / elementId(n) return the node’s UUID (as a string handle); there is no Neo4j-style integer id, so WHERE id(n) = <number> raises an error rather than silently matching nothing (compare against a string element id). A relationship’s id(r)/elementId(r) is its canonical <start>:<end>:<type> handle — unique and stable, but there is no relationship-id index, so a WHERE elementId(r) = … lookup is a scan. The model is id-less by design and cannot store two parallel edges of the same type between the same endpoints. A second CREATE of such an edge is rejected with Neo.ClientError.Schema.ConstraintValidationFailed, before anything is written — the existing edge keeps its properties. Bulk import paths are not covered by that rejection and still overwrite. See Data Model → Parallel edges: what actually happens for the exact behaviour and how to model repeated relationships, and Element identity.

  • No triggers, no stored procedures, no server-side scripting.

  • shortestPath and allShortestPaths are implemented for unweighted traversal. This does not imply support for every Neo4j path-selector shape, weighted shortest-path syntax or the full APOC-style procedure library. Named MATCH supports a single relationship segment with minimum zero or one. Attached MATCH predicates, relationship bindings/properties and additional relationship patterns in that clause are rejected. A post-WITH filter does not search for longer admissible paths. Common endpoints with minimum zero return an empty path, unlike Neo4j with its default common-node prohibition. See Named shortest paths.

  • List predicates (ALL/ANY/NONE/SINGLE) have native planned expressions for their source and condition, with short-circuit three-valued evaluation. Their conditions support XOR with precedence NOT > AND > XOR > OR. XOR evaluates both operands once; a NULL operand yields NULL. Native Boolean-value wrappers preserve strict type errors and keep nested subqueries in the planned path. Further index/hash-join rewrites and general decorrelation across outer rows are not yet implemented. Quantifiers still run per outer row; lists whose elements are nodes/relationships retain the compatibility-reference gaps.

  • Pattern comprehensions compile the pattern, filter and projection together. Matches are consumed one at a time into the result list; match and path-hydration failures abort the query rather than returning partial results. The list is bounded by PATINADB_MAX_AGG_ROWS; exceeding the bound raises ResourceExhausted. Execution remains correlated per outer row.

  • Repeatable EXISTS expressions can reuse Boolean results for repeated typed dependencies within an operator, sharing COUNT’s bounded auxiliary budget. Distinct keys may cost more; complex tails still materialize on cache misses. Unknown dependencies use full-row keys. Writes and unproven effects do not gain cross-invocation reuse; this is not general decorrelation.

  • EXISTS and COUNT retain accepted trailing WITH/RETURN clauses in their subplans: filters, DISTINCT, WITH-SKIP/LIMIT and aggregation affect the final rows. Outer variables remain available after WITH, and assigning a different value to an outer name is rejected. Missing parameters are checked even when the outer query has no rows. Full pipelines can materialize intermediate rows and are subject to the existing memory bounds; this does not add support for every Cypher subquery syntax or decorrelate repeated seeds.

  • COUNT { pattern [WHERE ...] } uses a seeded subplan in projections and comparisons, including correlated WHERE filters (#1230). Its scalar evaluation counts matching rows as they arrive rather than collecting them. This does not perform general decorrelation. Eligible deterministic projections may reuse a complete projected row when every proven input dependency has identical typed values within the same operator and snapshot. Unused outer columns can be omitted from the cache key; dependencies include all projected expressions, pattern seeds and grouped-value slots. Unsupported dependency shapes keep the complete input-row key. The memo has an 8 MiB serialized-data budget and the aggregate row cap; oversized/uncached inputs keep normal execution. Volatile expressions, writes and unproven effects disable reuse. Individual COUNT expressions can also reuse their own integer results by proven typed dependencies while unrelated outputs vary; unknown forms retain full-row keys. Graph patterns and trailing subplans qualify under the same effect proof. Different keys still execute separately unless the independent equality-scan path applies. Scalar result entries and equality tables share one auxiliary entry/byte budget; errors are never cached. COUNT expressions in a projection, filter, aggregation, sort or UNWIND source, including nested expression containers, can additionally reuse bounded equality counts from the first independent node scan or fixed-length graph path with equality between proven snapshot-stable inner-only and outer expressions. The inner expression may combine properties from multiple inner bindings; mixed inner/outer dependencies retain ordinary execution. Outer properties, parameters, arithmetic and containers are supported when independent of all inner bindings; direct id()/elementId() comparisons retain their special ordinary path. All such tables share one auxiliary budget per operator and preserve lazy evaluation and lexical bindings. All named nodes and relationships must be independent of outer bindings. A snapshot-stable inner-only leading AND prefix is admitted without reordering; false skips the equality, NULL retains its error evaluation. Trailing or unproved predicates remain ungrouped. Repeated names, variable-length paths, property constraints, indexed/seeded patterns and more complex inner plans are not admitted. Unsupported values or auxiliary-budget exhaustion retain normal execution; the first scan still incurs grouping work. The auxiliary table has a separate 8 MiB accounting budget and the aggregation entry cap. See query planning for exact admission and accounting. Filters include ordinary/seeded WHERE and inner COUNT/EXISTS predicate loops; each inner invocation owns its context. Serial aggregation shares one context for arguments, grouping and group-key output. Parallel aggregation uses separate worker contexts and a later finalization context on the pinned snapshot. Budgets apply per context, not globally across workers. Full sort and bounded top-K retain one context across sorting windows after proving the input and sort expressions repeatable. Distinct keys may incur overhead; unknown callers keep ordinary evaluation. UNWIND expression sources retain per-source contexts within one repeatable APPLY execution; nested invocations do not share them. Existing expansion caps still apply. This is not general decorrelation.

  • String properties only (matching Neo4j). No numeric/range terms inside the full-text query string.
  • No phrase slop / proximity — "a b" matches only exactly-consecutive terms; "a b"~3 is not supported.
  • No highlighting or snippet extraction.
  • Prefix/fuzzy expansion is capped at 256 terms per term to bound cost; very broad prefixes silently match only the first 256 dictionary expansions.
  • Postings are updated read-modify-write with no segment merging. This is fine for typical write rates but is not engineered for very high write throughput over a large indexed corpus (a follow-on — GH #1234).

Storage & scale

See How Storage Works for the layout, caches and memory model these limits refer to.

  • Single-machine storage. Each node holds the full graph; there is no horizontal sharding of one graph across machines. The cluster replicates, it does not partition.
  • The whole graph is on one B-tree file per database (patinadb.redb). patinaDB targets small-to-medium graphs, not multi-terabyte datasets.
  • Time-travel speed (not memory) grows with delta distance. Snapshots are captured and reconstructed as a streamed O(chunk) record run — building a periodic snapshot and rebuilding a past state both use bounded memory (the reconstruction runs into an on-disk temp store, not RAM), so the graph is not capped by memory. What still grows is time: reconstructing a point far from the nearest snapshot replays a longer delta chain, so frequent snapshots keep far-back queries fast. A narrow AS OF read now scales with what the query touches, not the whole graph, once its snapshot window is warm (RFC-0011 increment 1): the nearest snapshot is materialised once and shared across every time-travel read in its window, and a query reads through it lazily with only the in-window deltas overlaid — so history browsing / AS OF dashboards / diff exploration over a large graph are fast. The first read into a cold snapshot window still pays the O(graph) snapshot materialisation (cold O(query) for point reads is RFC-0011 increment 2).
  • A plain LOAD CSV … CREATE buffers into one transaction. The CSV source streams row by row, but a bare (un-chunked) write buffers all resolved ops into a single transaction (on the server, one Raft entry) before committing — so a very large LOAD CSV … CREATE has an O(rows) memory cliff. Wrap it in CALL { … } IN TRANSACTIONS [OF n ROWS] to chunk it into many small commits with O(n) memory — one engram per chunk, and on a cluster one client_write per chunk. For a large columnar file already on the server’s disk, POST /mgmt/upsert (index-accelerated match-or-create, chunkable) is faster still. LOAD CSV … RETURN row (read) streams end-to-end. See Bulk Loading & Import.
  • Some sort paths are O(N). Single-key ORDER BY uses an index fast-path, and multi-key sorts over a single labeled node — any per-key direction mix, plain field keys — are served index-side by the covering-compound (same direction) or leading-key partial-prefix scan (subject to cardinality stats, no covering index needed), with or without LIMIT. A covering compound index does not disable the mixed-direction partial-prefix path (#1233). The general executor’s O(N) sort still covers the rest: expression sort keys, DISTINCT sorts, traversals/multi-node patterns, label-less MATCH (n) ORDER BY n.p, mixed/unencodable sort columns, and shapes the stats gate declines — a follow-on — GH #1233 — see Query Planning.
  • On-disk format evolution needs a migration or a dump/reload. Each database records an on-disk schema version. Because the storage encoding is non-self-describing postcard, appending a variant to the end of a persisted enum is compatible and needs nothing, but a struct field add/remove/reorder, an enum variant insert/reorder, or a key/value byte-encoding change is breaking and bumps the version. An older database is not unconditionally bricked on open: if a migration step is registered for the version boundary it is upgraded in place; if none is registered the open fails loud and points you at the dump/reload path — export with the old build via the portable backup (GET /mgmt/snapshot), upgrade, then import (that backup is JSON and cross-version by construction). A database written by a newer build than the running binary is always rejected (there is no downgrade).
  • Cross-tree atomicity is guaranteed by the embedded B-tree storage engine, whose write transactions are natively cross-tree atomic. A commit, a Raft-applied entry, and a snapshot install each land as an all-or-nothing transaction of any size: a crash before the terminal durability barrier rolls the store back to the prior root. This is proven non-vacuously by the crash-recovery harness (a torn cross-tree flush at the property↔value-index seam recovers atomically). The Raft leader resolves a data-mutating query against a throwaway mirror and applies the resolved batch as one crash-atomic engram, so a very large single-query write never partially mutates the live graph.

The server

  • Native TLS (--tls-cert/--tls-key) covers REST + management + peer RPCs and the Bolt endpoint (bolt+s:// / wss://) (Authentication & TLS). It’s opt-in; without it everything is plaintext (terminate at a reverse proxy instead).

  • Firewall the Bolt port regardless of TLS. Issue #440 closed the unauthenticated “slowloris” connection-exhaustion vector: a silent, a stalled-mid-handshake, or an idled-after-HELLO socket is now bounded in time by --bolt-handshake-timeout-secs (default 10) / --bolt-idle-timeout-secs (default 300) and can never spend the --max-bolt-connections permit budget (default 1024, acquired only after a successful LOGON — see Connection resource limits). The Bolt endpoint is nonetheless still best run reachable only from your trusted backend network, not the open internet — every other resource (accepted file descriptors, per-message CPU) is still finite.

  • RBAC: global + per-database roles + per-label grants (row-filtered where provably safe) + optional strict relationship-type/property modes + optional closed-mode tenant isolation. Per-user accounts with admin/writer/reader roles, per-database overrides (GRANT … ON DATABASE …), and per-label READ/WRITE grants (GRANT READ|WRITE ON <db>:<Label> …) exist (Authentication & TLS). By default (open mode) a user’s global role reaches every database, including one it holds no grant on; --rbac-closed opts into real database-level deny for non-admins (see Authentication & TLS). Per-label enforcement for a read query is row-filtering, not a blanket reject, wherever it can be proven safe: a read that touches only granted labels returns full rows; a read that would observe any ungranted label (e.g. a traversal into it) returns an empty result — as if the ungranted nodes didn’t exist — rather than every property/value on the granted side being withheld one by one; only a read the analyzer cannot statically classify (an unlabeled MATCH (n), a procedure CALL, or a query that projects a whole matched entity that might carry an ungranted secondary label) is rejected outright, since row-filtering it could otherwise leak data through a side channel. A write touching an ungranted label is always rejected (a write can’t be partially applied). Two more layers are opt-in and reject-based (not row-filtered): --rbac-rel-grants (relationship-type grants — without it, a label-scoped user still reads all edge data) and --rbac-rel-property-grants (per relationship property). Per-property privileges on nodes are not implemented — a possible follow-on (GH #1231), deliberately unscheduled until a concrete requirement shows up: the design is mapped and the sensitive-property case (RETURN p.salaryField on a granted label) is otherwise covered only by the whole-entity rejection above or by application-side authorization. Auth is fail-closed: an empty password refuses to start unless you pass --insecure-disable-auth.

  • Security audit persistence is asynchronous and node-local. Authenticated write/admin/DDL operations and authorization denials feed a recent in-memory ring (GET /mgmt/audit), a background durable writer, and the patinadb::audit tracing target. Committed entries survive restart, but request responses do not acknowledge audit fsync. Hard crashes, queue saturation or storage failures can lose events. The log is not Raft-replicated; successful reads are not generally audited. See the audit contract.

  • No encryption-at-rest. The on-disk graph and persisted audit records are not encrypted by patinaDB — use OS-level disk encryption. This is a deliberate product decision, not pending work: OS FDE is a REQUIRED operator control (see Security → Encryption at rest; GH #381, completed).

  • No online membership reconfiguration UI — cluster changes go through the /mgmt/* HTTP endpoints by hand.

  • Writes are linearizable through Raft, so write latency includes a consensus round trip on a multi-node cluster. (A single --bootstrap node has no such cost.)

  • Reads are served from a node’s local applied state by default. On a follower/learner this can momentarily lag the leader. For a read that reflects all committed writes, request "consistency": "linearizable" on /cypher (leader-only, one round-trip — see High Availability). This knob is REST-only; the Bolt path always reads locally.

  • Explicit Bolt transactions are snapshot-isolated, not serializable — and your client MUST retry the conflict error. BEGIN pins a consistent snapshot (repeatable reads); COMMIT is first-committer-wins (a write-write conflict against a commit made since the snapshot — or a COMMIT that loses leadership mid-request — is rejected with the transient Neo.TransientError.General.TransientError), which prevents lost updates. This is a hard requirement for concurrent multi-writer correctness, not an edge case to shrug off: use your driver’s managed-transaction API (execute_write/executeWrite, or equivalent), which every official Neo4j driver retries automatically — a client that treats it as a hard failure will see avoidable errors under write contention even though the database itself never lost or corrupted any data. (Earlier patinaDB versions surfaced this as Neo.TransientError.Transaction. LockClientStopped — the official Python driver’s error-rewrite map reclassifies that specific code to a non-retryable ClientError, and the Java/JS 4.4-line drivers exclude it from retry by name too, so a routine conflict reached the application as a hard error under session.execute_write()/executeWrite. Fixed: patinaDB now emits the generic transient code every official driver variant retries.) Server-side, the same conflict-and-retry mechanism ALSO governs pipelined autocommit writes internally (a bounded number of server-side re-resolve-and-propose attempts before it, too, surfaces the transient code) — but a plain autocommit RUN is never automatically retried by the driver itself, in any version, since it isn’t wrapped in a managed-transaction function; a backend issuing autocommit writes under contention must still implement its own retry at the application level. Snapshot isolation does not prevent write skew (a read-write conflict with disjoint write-sets), and it is not serializable and not linearizable. For an invariant spanning rows one transaction reads and another writes, use a single autocommit statement, a sentinel node your transactions all touch (forcing the conflict to be detected), or an application-level guard. See Bolt & Neo4j Browser.

  • An explicit (BEGIN…COMMIT) transaction’s snapshot base is fast for a selective read, and still has a residual cost for a genuinely-scanning one at very large scale. Earlier, a managed transaction rebuilt its whole snapshot-isolation base from scratch on every statement, which under concurrent write load could mean nearly every transaction re-materialized the entire graph. That base is now shared and incrementally maintained, and a further fix removed a redundant planning-time cost for the common case — so a selective managed read (a point lookup, an indexed match) stays fast and roughly flat as the graph grows, even under heavy concurrent writes. The scanning case is now covered too: an explicit transaction reads its snapshot base through an MVCC read handle pinned at BEGIN, so a large scan inside a transaction no longer copies the touched range (measured ~1.4× faster on a scan-in-transaction workload — bench_mvcc_snapshot_vs_reconstruct_scan; see Caching for how to size memory around it).

    Keep explicit transactions short. An open transaction pins a storage read version for its whole lifetime, which holds back reclamation of pages superseded by concurrent writes. This is the ordinary MVCC trade-off, but a transaction left open for minutes under write load will grow the on-disk file. Commit or roll back promptly; the idle-transaction reaper (--idle-tx-timeout-secs, default 300 s) is the backstop, not the plan.

Time travel

  • Reads only. Time travel never writes. To bring a past state back to the live graph, use CALL patinadb.restore('<id>') — it promotes that state to HEAD as a new engram (append-only; see Engrams).

History epochs (RFC-0038)

See History Epochs.

  • The archive is a node-local filesystem directory that every member must see under the same path. There is no object-store (S3) backend.
  • A hot release needs a fresh receipt from every member, learners included. One offline member holds every release up.
  • A tag or pin inside a range blocks its release. Tags cannot be created in released history, and a fork into released history is refused.
  • Collection below retention requires a replicated metadata proof verified on the collecting node. New members use that proof to complete their history metadata after full archive deletion; expired states remain unavailable. If an older installation already collected the archive, a member with complete retained metadata must publish the proof. Without either source, backfill remains incomplete and retries (#1428). Proof publication requires protocol 20.
  • After a release, CALL patinadb.squash and prepared squash jobs are refused for that database; only a retention squash whose cutoff lies above the released range runs.
  • Cold reads hydrate the whole epoch segment up to the target into a temporary store, at most two at a time per process. Their latency, the export cost, archive size and the disk space a release returns are unmeasured.
  • A references_only backup is JSON-only and needs the archive at restore time. Its archive hold stays until an operator removes it. An offline patinadb-admin archive backup cannot read the archive and fails for a database with released history.
  • History restore, including a reference-only release index, requires all members on peer protocol 21 and replicates bounded encoded blocks (#1419). The decoded history still consumes proportional memory. Graph restore remains chunked; its rollback does not undo schema changes or an already published timeline.

Compatibility

  • patinaDB implements a subset of Neo4j. The Bolt endpoint and the system shim are sufficient for the official drivers and the Neo4j Browser, but Neo4j-specific admin surfaces, APOC, GDS, multi-database access control, and enterprise features are not present.

Ontology view (RFC 0039)

CALL patinadb.ontology() and the console’s Ontology screen describe only the current committed state of one database, exactly and synchronously. Historical (AS OF) views and comparisons, sampled discovery, discovery jobs or paged results, SHACL export, MCP access and scoped (filtered) ontologies for restricted principals are unavailable in this release. Discovery is off by default (PATINADB_ONTOLOGY_DISCOVERY). Default limits refuse very wide schemas (more than 50 000 records or 32 MiB encoded) instead of truncating them. A principal with any label-, relationship-type- or relationship-property -scoped grant on the database is refused rather than filtered. Unnamed single-property CREATE INDEX ON :L(p) markers are not reported as declarations (they are not label-scoped), uniqueness constraints are not validated against the data, and nested list/map element shapes are not described. A console instance serves one database; there is no database selector on the screen. Collection reads every node and relationship; performance under concurrent load and with a cold page cache is unqualified. See Database Ontology View.

Tracked work

The limitations above that are called out as “not yet” or “a follow-on” are on the roadmap. Each is listed here with the wording it carries above, so you can find it:

  • general decorrelation — eliminate repeated subquery work across outer rows, preserving correlation, multiplicity, NULLs and volatile expressions (GH #1230).
  • deliberately unscheduled — per-property RBAC grants on nodes (relationship properties already have theirs): designed and parked, scoped when actual demand arrives. (GH #1231)
  • element-type refinement (bare LIST only — the LIST<T> form). (GH #1232 — note: the body calls this “Not supported” while this section lists it as roadmap; which of the two is true is an open product decision, named in the issue.)
  • Some sort paths are O(N). — the O(N) sort fast-path integration. (GH #1233)
  • Postings are updated read-modify-write with no segment merging. — the deferred full-text items above. (GH #1234)

If a limitation here blocks you, reach out to your vendor or account representative to check on its status.

IAM qualification boundary (RFC0037)

IAM implements local principals and profiles, provider-backed OIDC/OAuth and SCIM paths, a replicated authorization write fence, and administrative-restore quarantine. Its bounded local test matrix has passed, but this is not release qualification. Only Keycloak 26.8.1 with the documented recipe (including Console sign-in) and the official Python neo4j driver 6.2.0/5.28.2 have been checked against a real IdP; Microsoft Entra ID, Okta, other drivers and real SCIM provisioning services remain unqualified. SCIM was checked with the generic scim2-tester client and has documented deviations: no POST /.search, eq filters only (case-sensitive), a single primary email, active cannot be removed, unknown group members are a retryable 409, and externalId is immutable once provisioned. SCIM over plain HTTP is not refused on a node without TLS. No authentication, authorization or revocation timing has been measured. Console sessions are node-local in-memory and do not provide shared browser-session continuity across nodes. Strict freshness is opt-in for external principals, not a default cluster-wide instant-revocation guarantee. The current candidate adds per-delivery authorization revalidation to Console /changes SSE, including replay, live events and resynchronization, with terminal errors and buffer clearing on expiry, revocation or unavailable checks. Targeted expiry and revocation regressions passed in the source-bound local validation. This does not promise instantaneous cluster-wide revocation before a change is committed; PR CI verification remains. The current API has no REST explicit transaction-handle interface; /mgmt/transactions manages the node-local Bolt transaction registry only. See IAM and RFC0037.

PR #1426 review corrections passed fresh local regression, crash and acceptance checks; PR CI remains required before merge. Admin Console queries require recent authentication even for reads; pending OIDC login cookies are invalidated on Console restart. Portable identity restore requires protocol 19 and a single identity bundle within the entry-size limit. A portable restore merges backup IAM records over the target, so an identity deleted after the backup was taken comes back (providers stay quarantined until released) and target suspensions/disables take the backup’s values; the intended merge policy is an open RFC0037 question. The Identity & Access screens edit records as structured JSON forms; the RFC’s selector/review workflow, user search and “affected users” preview are not implemented yet. See IAM.

The LSQB SF0.1 follow-up on PR #1410 still has original-query timeouts and Q1/Q2/Q5 regressions (see the repository coverage ledger). UUID entry and typed bound-target corrections alone do not qualify the query engine’s performance. Direct label-registry read reuse is under validation; it adds no setting or changes to stored label IDs, query results, memory caps or durability.

On PR #1410 candidate9c8929aa, original SF0.1 Q3 now completes with the exact published count in the native comparison. The full suite still has timeouts and Q1/Q2/Q5 regressions; no general performance qualification follows. See the repository’s LSQB coverage ledger for the first calls, repeats and conditions.

Epoch datetime map boundaries

datetime({epochMillis: ...}) accepts integer Unix milliseconds and an optional timezone. Calendar storage uses signed32-bit epoch days; unsupported extreme instants and override years are rejected, with a tighter bound for named zones. Calendar overrides into nonexistent local daylight-saving times raise an error instead of Neo4j’s forward adjustment. See Cypher.

Schema 8 upgrade (RFC 0040)

Schema-7 stores migrate in place to schema 8; older incompatible schemas retain existing export/re-import requirements. Inline secondary labels are enabled by default. The first open of an existing store without READY rebuilds adjacency values once using sorted staging and atomic table swaps. On the private SF1 copy this took about 95 s, with +0.68% allocated space after offline compaction (RFC 0040 “Increment 1 measurements”, #1442). Temporary space is needed during the rebuild. PATINADB_INLINE_LABELS=0 skips automatic ensure; it does not undo an existing READY marker or stop writer maintenance. See Day-2 operations. Schema-8 physical snapshots require protocol 22 (snapshot format 8) on every member. Mixed-version clusters fall back to logical transfer. Clusters with a causal domain need every member at protocol 21 or later for the logical causal format (7); below that they cannot create new snapshots. Plan that upgrade before relying on snapshot-based catch-up. Downgrading a schema-8 database is not supported.

Supply-chain gate rollout

The full-repository license check covers demo and fuzz lockfiles. The demo uses pinned Vite7.3.7 without the MPL lightningcss dependency; the reviewed libfuzzer-sys0.4.13 NCSA conjunction has an exact package/version exception. Other versions and unapproved licenses still fail. Runtime application dependencies still reject unknown or unapproved licenses. The approved Docker OS profile admits only exact reviewed base/package/source/ notice identities; it never substitutes for notices and exact source delivery. The aggregate branch-protection check is activated only after the updated workflow passes on main. Full multi-platform validation remains required; synthetic tests alone do not qualify release images.

Planner entry-cache accounting (#1467)

The optional A2 region planner.entries uses estimated ownership bytes, not an allocator/RSS ceiling. Keys are reserved before copying; unusually deep values and function/expression literals without a sizing model skip reuse and use the normal planner. Per-database statistics omit construction and evicted entries still pinned by readers; the global level byte count includes both. This change has correctness controls, but no new throughput or memory-pressure soak claim.

The HTTP pre-proposal outcome receipt is deliberately narrow: only non-chunked upserts and REST autocommit data writes refused during private snapshot admission carry write_outcome: "not_submitted". Reads, chunked writes, schema commands and post-proposal failures do not gain this receipt. It is not a general rollback certificate for arbitrary 503 responses or chunked loads. The new five-hour/15–20GiB-per-node qualification under 5 GiB RAM is still open. The source-bound 2e9b7d67 small smoke passed, but the subsequent full attempt stopped during chunked bootstrap before mixed load (#1483).

The chunked graph validation wait covers only read-only snapshot admission before the next proposal, within the existing query deadline. It is not automatic recovery or rollback of a partially committed import. The latest full attempt stopped during bootstrap with such a partial load; no five-hour qualification is claimed (#1483).

The next d98f7c0e smoke stopped earlier in bootstrap on an uncertified Cypher snapshot refusal. Retained graph-copy readback matched the acknowledged state on all five nodes, but does not retrospectively certify the unknown write’s outcome. The REST Resolve-only receipt correction still requires fresh runtime qualification (#1483).

The standalone soak backend has a pending pre-RUN timeout correction: only atomic prevention of the first write statement certifies non-submission. Timeouts after submission remain ambiguous and require readback. The latest 4-GiB-per-node run failed before completing three hours; this correction has not yet passed a new full pressure qualification.

Pipelined write snapshot admission

Before executing a pipelined Cypher write, Bolt and REST may wait for a complete native read snapshot within the existing query deadline. Only snapshot acquisition is repeated: this does not replay the query, a WASM function, or a submitted mutation. Other storage errors propagate. If the query timeout is disabled, or the calling thread already owns a deferred native write, an unavailable publication is refused rather than waited on. The network server’s existing default query timeout remains 300 seconds; no new setting is added. The focused publication-race regression is verified; renewed full pressure qualification of this change is still pending.

Fork publication under concurrent access

A fork becomes visible in its registry only after its copied graph and genesis initialization succeed. Registry lookups can wait while that initialization holds the registry publication lock; source reconstruction precedes the lock. A failed initialization removes only the new target directory owned by that attempt. This live-publication guarantee is not crash-atomic fork creation.

Seeding the target writes its graph, fulltext and vector indexes, genesis history and HEAD under one durability barrier (#1511). A crash during seeding therefore leaves an empty target with no HEAD, never a partly seeded one. The empty target directory itself can remain after such a crash. A local fork of the soak’s history_probe copy went from 94 to 16 syncs; opening the new database store accounts for most of the remaining 14 (#1511).

Fork preparation keeps its target unpublished until graph and history are complete. Namespace changes are serialized; reads of existing databases do not acquire the fork lifecycle lock. The fork correction is covered by a gated lookup regression and namespace-race tests. CREATE and physical install retain their existing map-lock duration; concurrent namespace mutations can still wait for fork completion. The registry-lock change passed the full pressure soak 0185c2321912 (#1495); the single seed barrier is newer and not yet soaked.