specs
Every constraint and index the store depends on.
Constraints.
ExtractionRun(contextId, runId)— the tenant-qualified natural key. Two tenants minting the same run id are two runs, which is the whole reason the tenant is in the key.ExtractionRunInvocation(contextId, runId, invocationIndex, attempt)— the deterministic child key. A retried attempt lands on its own row, and a replayed write of the same attempt upserts in place.ExtractionRunTerminalWrite(contextId, runId)— at most one terminal write per run, ever. This is the compare-and-set. Two writers that both read a run asRUNNINGboth try to create this node, and the database lets exactly one of them commit.
Indexes. Each one is what a specific read seeks on, and none of them carries status or the terminal fingerprint — deliberately, because the compare-and-set reads both after taking its lock and an indexed property could be served from an index entry read before it.
(contextId)—runsInContext. A composite index cannot stand in: Neo4j will not use one for a predicate on only its leading property, so without this the tenant predicate is a label scan. Same reasoning as theProposition(contextId)index.(contextId, rootRunId)—runsOfRoot, the read the denormalized root exists for.(contextId, parentRunId)—childrenOf, one hop down the parent axis.(contextId, startedAtEpochSecond)— the paging sort key. Pages are newest-first by start time within one tenant.ExtractionRunInvocation(contextId, runId)— reading one run's attempts. The four-property constraint index is keyed on the whole identity and is not a substitute for a lookup on the first two.