ADR 0028: One transaction owns graph mutation effects
ADR 0028: One transaction owns graph mutation effects
Section titled “ADR 0028: One transaction owns graph mutation effects”Implementation: Implemented in #1151, which closed #1010 and landed this record in the same change. Related: #1010; ADRs 0025 and 0026
Context
Section titled “Context”Cypher’s statement driver buffers writes, stages one rewrite, and assembles counters and receipts. Its facade separately persists the observed runtime catalog and publishes a generation. Analyst write-back duplicates catalog interning, property staging, receipt construction, snapshot capture and rollback. A property-write staging failure and an error after CURRENT publication must not be handled as the same transaction outcome.
This is a correctness-first durable mutation boundary. Existing public Arrow results, receipt meanings, checked identities and durable formats must remain compatible. Clause order, pending-write visibility and pure-append adjacency segments must also survive the extraction.
Options
Section titled “Options”- Share only the API publication wrapper. Small, but leaves duplicated staging, catalog, counters and receipt invariants; it does not satisfy #1010.
- Extract a supported execution-owned mutation transaction and supply an API publication adapter. Reuses existing owners without a reverse dependency.
- Move all transaction policy into storage. This would pull execution receipts, runtime observation policy and API domain publication into the wrong layer.
Decision
Section titled “Decision”Choose option 2, without adding a crate or a durable transaction format.
graphforge-exec::mutation owns a MutationTransaction: the working catalog
snapshot, neutral effect/counter accumulation, staged rewrite, and commit/abort
state. Both entry points use its property-write recording and receipt builder.
The Cypher statement context retains only clause/frontier evaluation and
pending-write visibility; its buffered topology/property operations feed this
same transaction. A property-only analyst transaction does not open a topology
writer or scan all label memberships merely to use the shared machinery.
Catalog interning occurs against a transaction-owned working catalog. Cypher’s binder receives that catalog before mutation binding; analyst property interning uses the same owner. The catalog’s persisted batch joins the graph rewrite, rather than being written independently after a successful Cypher commit. The live catalog is installed according to the final publication outcome. Ordinary read-query observation policy is unchanged.
The API supplies a small publication adapter using its existing generation and domain-participant methods. It does not call private executor phase functions. The adapter captures the authoritative parent generation, publishes the neutral receipt, restores or reconciles the workspace when required, and installs the resulting property/ordinal authority. Transaction orchestration, rather than each verb, invokes these operations. Standalone execution retains its existing local staged-commit behavior through the same core without API publication.
The shared transaction owns the complete sequence:
- Acquire existing write admission and establish parent/catalog state.
- Bind or validate against the working catalog; accumulate graph operations, existing property-counter semantics and deterministically ordered neutral effects.
- Stage graph and catalog changes together. Reuse existing RewriteBatch and topology/UUID-index commit primitives, including adjacency delta behavior.
- Commit the local rewrite, then publish through the API adapter when present.
- On success, install the catalog and refresh/invalidate retained graph resources through one completion path. Preserve selected-resource authority.
- On any error, including a local rewrite commit failure, use the same abort path. Restore the parent workspace/catalog only while the parent remains authoritative. If publication already advanced CURRENT, preserve committed data and reconcile the live catalog/resource authority; never undo a published generation because a later operation returned an error.
Every GraphForge facade, including in-memory construction, initializes a project generation before executing writes. Its ordinary publishing operations therefore have an authoritative parent, including the first write to an empty graph. Standalone exec targets and unpublished transaction workspaces can lack an authoritative snapshot of their current contents. Those paths require a private disk-backed rollback copy of the admitted workspace, with validated file inventory and streaming/reflink materialization, before any local commit. They must never silently run without rollback merely because CURRENT is absent. This fallback does not build a whole-graph Arrow byte envelope in memory.
Rollback uses the authoritative committed generation where available; analyst write-back no longer captures a whole-workspace Arrow snapshot for this purpose. Errors determining publication state fail closed; they do not authorize a speculative restoration. Existing project publication remains the durable atomicity boundary, rather than a second commit protocol in exec.
Validation and consequences
Section titled “Validation and consequences”Baseline tests first pin repeated SET, CREATE-then-SET and no-op counter behavior. A shared Rust-facade test runs equivalent Cypher SET and real rank/cluster write-back on matching seeded graphs. It compares normalized receipt fields, existing property counters (including repeated SET and CREATE-then-SET), catalog entries, stored values and generation effects. Receipt capture stays test-internal; no new public facade result is introduced.
The same matrix injects failures during staging/local commit, before CURRENT, and after CURRENT. It checks pre-publication rollback of both data and catalog, post-publication preservation, resource invalidation and durable reopen. Include an existing nonempty graph, no-op/empty write-back, repeated SETs, mixed Cypher CREATE/SET/REMOVE/DELETE/MERGE, and semantic composition routing. Existing query parity and bounded cache tests remain required.
This is one concern under #1010. Implement the shared state/staging core first, route both entry points through it, then delete the duplicated catalog/receipt/ rollback paths only after the common acceptance matrix passes. The API remains the owner of generation/domain publication and exec remains independent of API.
Compatibility details verified during implementation
Section titled “Compatibility details verified during implementation”Exploratory Cypher SET/property-reference observation uses an unowned runtime property entry; CREATE property literals alone do not intern a property entry. Analyst write-back already supplies its selected label as owner. The transaction preserves these caller inputs while owning their private catalog and publication. Tests check each exact owner and persistence; they do not claim these catalog entries are identical. Neutral receipts and all side-effect counters are compared exactly for equivalent preexisting property state, including Cypher’s replacement counter. The property rewrite reports replacement facts from its existing before-map without adding a read pass.
Admission pins one destination, mode, semantic contract and immutable catalog batch. A transaction prepares one statement or one analyst update; a second preparation fails before evaluation and leaves the first prepared state intact. Catalog staging and final installation use the admitted batch, not a later read of a mutable catalog alias.
A failed restoration retains its rollback directory and marks the shared execution-resource health owner unavailable. New reads/writes and already-returned lazy streams check that owner. The facade and standalone session paths use the same health semantics. A fresh facade can reopen the untouched durable parent; the failed owner does not silently resume. Checkpoint copying bounds memory, not total scratch-disk use. It adds no durable record or recovery protocol.
Recovery advances an owner-local epoch before touching workspace files. Existing lazy streams check that epoch before and after each inner poll, so a poll that overlaps restoration cannot emit a partially read batch. Successful restoration admits new work but invalidates old streams; failure remains permanent for that owner. Overlapping recovery is rejected, and completion cannot clear a separately recorded failure. The explicit multi-statement facade transaction uses the same abort adapter around its existing publication boundary, including errors after an earlier unpublished statement. Composite domain publication and its receipt remain owned by the existing facade publisher.
A standalone target that did not exist at capture stays absent until its writer runs, preserving the lowering snapshot’s empty-target contract. An early failure restores that absence without replacing the original inventory-less catalog; after writer creation, rollback verifies an empty materialized tree instead. Restoration validates the destination root as a non-link directory before any destructive walk. A substituted root is rejected and its rollback backup survives owner drop.