Skip to content

ADR-0018: Migration journal and recovery boundaries

Date: 2026-10-11. Status: implemented and verified locally and in hosted run 38108942707.

GB-015 follows the verified schema witness under M5.2, before M6 tooling. Build passes with zero warnings/errors; 442 unit and 128 live cases pass, including 64 new unit and 17 migration integration cases. Execution history and limits remain recorded in the implementation tracker.

Neo4j cannot combine schema writes and graph-data writes in one transaction, so a schema command and its history-node update cannot commit atomically. Individual constraint creation validates existing data and is atomic, which does not make a multi-step migration atomic. Transaction rules, constraint semantics.

Use an immutable versioned plan containing migration/predecessor IDs, ordered stable step IDs, typed operations and parameters, capability requirements, impacted elements, pre/postconditions and explicit rollback policy. Canonical serialization/checksums cover every execution-relevant field. Preview has no database access; validation reads without initializing schema or history; apply revalidates under migration ownership. No provider startup or ordinary EF operation runs migrations automatically.

Explicit initialization deploys reviewed unique keys for reserved control/history/step records and verifies exact definitions. Acquisition locks the database-wide control node before testing/updating a committed owner token and generation. Ownership has no automatic expiry: a paused worker can resume after a timeout, and generation checks on data/journal writes cannot fence separate DDL transactions. Recovery requires an explicit declaration that the previous process is stopped/fenced and outstanding work resolved. This coordinates cooperating migrators; it does not block ordinary application writers or unrelated administrators. Locking behavior.

Data steps lock/check ownership, evaluate conditions, execute a bounded parameterized transformation and record completion in one explicit transaction. Schema steps commit a Prepared journal state, execute one schema operation separately, verify its exact postcondition, then record completion. Index creation and ONLINE readiness are separate checks. A crash or uncertain response leaves durable ownership/pending state for reconciliation; no automatic replay, lease stealing, drop/recreate or compensating Down execution is implied. Same completed migration/checksum is a no-op; changed checksums conflict.

Review exposed two implementation gaps, corrected before initial live acceptance. Candidate elements must acquire a dependent property-write lock before fresh properties(e) scope/eligibility checks; checking before a blocked write could overwrite an application value committed while waiting. The live test observes an actual blocked transaction and verifies that the application value survives. Candidates are distinct and bounded to MaxRows+1; this is not a serializable snapshot and does not fence insertions. A fresh schema precondition must also be established before recording Prepared, otherwise recovery could incorrectly adopt schema that existed before the first attempt. The preexisting-schema rejection now passes a live regression.

Simultaneous initialization can cause Neo4j to abort one schema creator with Neo.TransientError.Transaction.DeadlockDetected. This remains an explicit caller-visible failure. Acceptance permits only that exact loser alongside a successful initializer and verifies that an explicit rerun converges to the same three unique constraints and one control record; no automatic write retry is added. Read-only validation checks the first unfinished step because later preconditions may depend on earlier transformations. Every step is checked during apply.

The M6 combined run reopened the initialization race gate: one unexpected InvalidOperationException was hidden by the test's old type assertion. Independent Neo4j 2026.09 source review established a reachable intermediate state: the kernel commits a backing index separately before committing its uniqueness constraint, and SHOW INDEXES can expose a null owner during this interval. See constraint index creation and owner reporting.

Initialization now polls only an exact reserved-name NODE/RANGE index with the expected single label/id property, null or expected owner and POPULATING/ONLINE state while its constraint is absent. Every poll is read-only and disposed before a cancellable delay. Full exact constraint/owned-ONLINE-index validation still precedes control writes or skipping DDL; an intermediate index is never adopted. Independent conflicts remain immediate errors, and a persistent ownerless index times out. IndexWaitTimeout bounds the polling budget; TransactionTimeout separately bounds an in-flight metadata request. No write retry or new automatic recovery is introduced. All 454 unit cases pass locally and hosted, including 12 new transition/timeout/cancellation/conflict regressions. The improved three-iteration live race test passed against the previous runtime in run 38111298123; the original exception did not recur, so its attribution remains unconfirmed. Run 38111997235 on edb24ca verifies this correction with all 134 integration/12 upstream tests and complete fixture cleanup, closing the reopened regression gate.

Recovery resolves data completion from its atomic journal record. For pending DDL, exact postcondition means recovered completion, exact precondition means eligible retry, and neither means conflict. Do not use IF NOT EXISTS to conceal name/definition conflicts. Destructive typed steps carry expected definitions and explicit intent; data rename/backfill is an explicit reviewed transformation. Down steps or a NoRollback reason are shown in preview; compensation is a separate operation. Constraint create and drop semantics.

Use selected-database explicit transactions, finite timeouts, step metadata, cancellation checks and async cleanup. Reject execution within an existing EF transaction. Managed retry callbacks, independently committing subtransactions and arbitrary procedures with external effects cannot provide the atomic data-step contract; large backfills require explicit bounded steps. Driver transactions, transaction limits, subtransactions.

Required acceptance includes deterministic checksums; no-write preview/validation; initialization races; exactly one concurrent owner; no timeout takeover; atomic data failure; partially committed DDL failure; crash-after-DDL reconciliation; uncertain data commit resolution; index population failure; stale-owner rejection after explicit recovery; precondition/no-rollback behavior; selected database/timeouts/cancellation; and narrow explicitly destructive steps. Executor tests must establish these boundaries before claiming migration support. EF CLI services and blind Graph Type replacement remain separate work, not implicit consequences of this executor.