Repository navigation
v0.16.0
GoGraph v0.16.0 — see CHANGELOG.md for the per-release entry.
0.16.0 — 2026-10-09
167 commits — 164 non-merge commits and 3 sprint merges — counted at 60c90d73, the
develop tip before release preparation. Three sprints delivered this window: 363,
Performance and Efficiency Laboratory 260924 (24 commits, merged at f984222f); 364,
Correctness and Security Laboratory 260928 (76 commits, merged at c63fd1fd); and 365,
Observe, Correct and Optimize MVCC, Store and LPG Exercise (64 commits, merged at 60c90d73).
1 085 files changed, 152 515 insertions, 14 958 deletions (git diff --shortstat v0.15.0..60c90d73).
This release breaks the exported API and changes the on-disk format. Measured by parsing every
non-test Go file outside internal/, examples/, cmd/ and any gen/, tck/ or testdata/
directory at both v0.15.0 and 60c90d73, and diffing the rendered declarations — a superset
of the API surface docs/semver.md defines: 4 822 exported declarations before,
5 160 after. 74 declarations changed signature, 4 were removed and 342 were added. Of the 74,
two are parameter renames only (wal.Open, wal.OpenFS) and one widens a parameter to an
interface the old type satisfies (recovery.ReplayWAL), so 71 re-signings and 4 removals are
source-incompatible. Under docs/semver.md a 0.y.z release absorbs breaking
changes in the MINOR digit.
Security
- Toolchain pinned to
go1.27.2(go.modtoolchaindirective, wasgo1.27.0). The
vulnerability database of 2026-10-08 lists 13 standard-library vulnerabilities ingo1.27.1,
9 reachable from GoGraph (net/http,crypto/tls,net/textproto,mime/multipart), from
GO-2026-6599 to GO-2026-6617. Withgo1.27.2,make vulncheck(govulncheck v1.8.0, source mode,
symbol level, 140 packages) reports none.
Changed — BREAKING: WAL v2 on-disk format
- The write-ahead log is segmented, position-chained and bound to its store (rmp #3020,
#2195, #3014). The design is docs/design-wal-v2.md and the format is
specified in store/wal/FORMAT.md. A store's log is now a 64-byte control
filewal.controlcarrying a random, per-store id, and numbered 16 MiB segments underwal.d/.
Each frame has a 36-byte header (frame version2) carrying its logical position, the length
back to its predecessor (the prev-link) and the store id. Recovery streams the segments and
refuses, each with its own error, a frame from another store (wal.ErrForeignStore), a frame
at the wrong position (wal.ErrFramePosition), a broken prev-link (wal.ErrPrevLink), a
segment gap (wal.ErrSegmentGap), a torn non-tail segment (wal.ErrTornSegment), and a
snapshot too old for or ahead of the retained log (recovery.ErrSnapshotTooOld,
recovery.ErrSnapshotAheadOfWAL). A checkpoint reclaims space by unlinking whole segments
below the oldest retained position, without the commit lock; it no longer rewrites the log's
suffix. - One-way migration on the first writable, clean open. A store written by an earlier release
keeps its single-filewal; the first writable clean open seals it with aLegacySealcontrol
record, writes the control file and continues in segments. The sealed legacy log stays readable.
A migrated store cannot be opened byv0.15.0or any earlier build: the seal is a version-2
frame, so an older reader refuses the directory instead of ignoring the segments. A torn legacy
log followed by segment frames is refused withwal.ErrLegacyNotSealed. Take a copy of the
store directory before the first open withv0.16.0if a downgrade must remain possible. - Node ids survive a reopen exactly (rmp #3021). Every commit marker carries an annex naming
each node id the transaction created, and recovery places those ids before it applies the
operations, so a rolled-backCREATEor the commit order no longer moves a node'sid()or
elementId()across a reopen. An operation naming an unbound key, or an id held by another key,
refuses the store (recovery.ErrUnboundNodeKey,graph.ErrNodeIDMismatch,
recovery.ErrCommitAnnexCorrupt,recovery.ErrIDRecordCorrupt). - Node ids are reserved ahead of use (rmp #3021). A node id is issued only below a per-shard
limit logged in aReserveIDscontrol record (no fsync); batches double from 64 to 65 536
(txn.DefaultIDBatch,txn.MaxIDBatch). Checkpoints store the per-shard high-water marks in a
new snapshot component,nodeids.bin, and the snapshot manifest moves to version 4 with
StoreID,WALFormatandWALRedoPosfields.v0.15.0reads manifests up to version 3 and
refuses version 4. Recovery refuses an id beyond its reservation
(recovery.ErrIDBeyondReservation). - Ids assigned but never born are holes, not errors. A capture no longer refuses an instant
with an open transaction, and never writes such an id as a tombstone. This fixes a node interned
at the capture instant and created afterwards throughAddEdgealone, which recovery brought
back dead. - A truncated WAL without its snapshot is refused (rmp #2990, #2991, #3002). Before the first
prefix truncation the WAL writes a durablewal.prefix-truncatedmarker, kept current by every
checkpoint whose snapshot is self-sufficient and written by the first clean recovery that finds
it missing. A marked directory without a snapshot manifest is refused with
recovery.ErrMissingSnapshot; before, it recovered as clean and silently dropped every commit
the checkpoint had folded. - Anonymous Cypher node keys come from a per-graph sequence (
lpg.Graph.KeySequence) instead
of a process-wide counter, so identical runs write identical logs.
Changed — BREAKING: exported API
- Removed:
store/wal.(*Writer).TruncatePrefixandstore/wal.ErrPrefixTruncateUnsupported
(segments are unlinked instead);store/recovery.Result.WALTailOffset(replaced byWALEnd,
WALLastFramePos,WALTailSegmentandWALTailSegmentOffset); and
store/snapshot.ErrCaptureNotQuiesced(a capture is no longer refused at an instant with an
open transaction). - Re-signed, in addition to the lists under rmp #2748 and #2947 below:
graph/lpg.Graph:RemoveNode,RemoveNodeLabel,DelNodeProperty,Reviveand
RestoreTombstonesnow returnerror.store/snapshot.ApplyTombstonesToGraphnow returnserror.graph.(*Mapper).LoadFromtakes a second argument, the per-shard high-water marks
(*[graph.MapperShards]uint64).cypher/ast.ExistsSubquery.Queryandcypher/ast.CountSubquery.Queryare nowast.Query
(a possibly multi-branch query) instead of*ast.SingleQuery, and
cypher/ir.TranslateSubquerytakes anast.Query.cypher/exec.MergeActionEvalKeytakes the action's ordinal as its first argument.
- Widened, source-compatible:
store/recovery.ReplayWALtakes awal.FrameSource, which
*wal.Readersatisfies.
Changed — BREAKING: behaviour
- One token limit on every layer: 65535 bytes (rmp #2748). A node label, a
relationship type or a property key longer thanlpg.MaxTokenLen(65535) bytes is
refused with an error wrappinglpg.ErrTokenTooLong, before any state change, by the
in-memory engine (graph/lpg), the WAL-backed store (store/txn, whose
ErrTokenTooLongis the same value), bulk import (store/bulkimport, before it
publishes) and both Cypher engines. Before, only the WAL refused such a name, so the same
graph was durable or not depending on how it was built.lpg.CheckTokenvalidates a name
before any write. A snapshot carrying an over-long token, which only a pre-bound bulk
import could write, now fails to open with that error instead of loading silently. - Signatures changed to carry the refusal:
graph/lpg.Graph:SetEdgeLabel,RemoveEdgeLabel,DelEdgeProperty,
SetEdgeLabelAt,SetEdgeLabelByHandle,DelEdgePropertyByHandle,
SetEdgeLabelByHandleID,SetEdgePropertyByHandleIDandDelEdgePropertyByHandleID
now returnerror;SetEdgeRelTypeAtSlotByIDandAddEdgeRelTypeOverflowByIDnow
return(bool, error).graph/lpg.WriteView:RemoveNodeLabel,DelNodeProperty,SetEdgeLabel,
RemoveEdgeLabel,DelEdgeProperty,SetEdgeLabelAt,SetEdgeLabelByHandleand
DelEdgePropertyByHandlenow returnerror.graph/lpg.LabelRegistry.Internandgraph/lpg.PropertyKeyRegistry.Internnow return
(id, error).graph/lpg/schema.Schema.RegisterLabelnow returns(lpg.LabelID, error).store/snapshot.ApplyEdgeHandlesToGraphnow returnserror.cypher/exec.GraphMutator:RemoveNodeLabel,DelNodeProperty,SetEdgeLabel,
DelEdgeProperty,SetEdgeLabelAt,SetEdgeLabelByHandleand
DelEdgePropertyByHandlenow returnerror.
- A direct write is a single-operation transaction (rmp #2947). A mutator of
graph/lpg.Graphcalled outside any transaction — and aGraph.Writerview over the
zeroWriteTx— now runs as an implicit transaction of its own: it conflict-tests and
claims exactly as a statement does, commits at one instant, and on a refusal aborts
and changes nothing. While another transaction holds an uncommitted write on what it
would change — a node's properties, labels or existence, an edge's adjacency entry,
or an edge's per-handle, per-ordinal or overflow records — it returns an error
wrapping the newlpg.ErrDirectWriteConflictand a*mvcc.Conflict. The refusal is
retryable. A refusal caused by another direct write is retried internally for up to
1 s first; one caused by an explicit transaction is returned at once. Before,
such a write returnedniland was lost when that transaction rolled back, and a
direct edge or node removal could be left half applied. Separately,Graph.ApplyVersionedand
Graph.BeginVersionedTxno longer claim the graph's ambient write slot: a direct write
made while one is open is no longer stamped with its record, so it is visible at once
instead of when that transaction ends. A direct call made inside anApplyVersioned
function is therefore its own transaction, not part of that bracket — write through
Graph.Writer(tx)there, as the documentation already requires. - Breaking: a direct write is never part of an exclusive bracket (rmp #2947). Inside
ApplyAtomically,ApplyAtomicallyTxandLockBarrier/ApplyInsideLocked, a direct
mutator call used to join the bracket's transaction, and so did a direct write from
any other goroutine made while the bracket was open: it was acknowledged, and then
lost when the bracket aborted. A direct call is now its own implicit transaction in
every context. It commits at its own instant, survives the bracket, and is refused
withErrDirectWriteConflictby the bracket's own uncommitted writes. A bracket whose
writes must land together writes throughGraph.Writer(tx)over theWriteTxthat
ApplyAtomicallyTxorApplyInsideLockedTxhands it;ApplyAtomicallyTxreturns the
conflict that doomed the bracket even when the function ignored the refused call's
error.ApplyAtomicallykeeps its signature: its function has no transaction to
write through, so it is for exclusive work that writes no versioned data.
WriteView.AddEdgeLabeledWithPropertyis new, so a labelled, propertied append can
be written through a transaction. - The adjacency refuses a write that would build on an uncommitted entry (rmp #2947).
graph/adjlistrefuses, with a*mvcc.Conflictand before changing anything, every
write — with or without a transaction, implicit or explicit — over an entry another
transaction published and has not committed. An undirected edge write takes both endpoints' shard locks in
ascending shard order and tests both entries before writing either; an undirected
RemoveAllEdgesFromgrows its locked set until it covers every neighbour, in at most
256 rounds. - Signatures changed to carry the refusal (rmp #2947):
graph.Graph.RemoveEdgenow returnserror, and so does every implementation.graph/adjlist.AdjListandadjlist.Writer:RemoveEdge,RemoveAllEdgesFromand
ClearEdgeLabelSlotsnow returnerror;RemoveEdgeByHandle,UpdateEntryAux,
SetEdgeLabelSlotandClearEdgeLabelSlotValuenow return(bool, error);
SetEdgeLabelSlotsAt,ClearEdgeLabelSlotsValueandSetEdgeLabelSlotsnow return
(int, error).Writer.AppendEdgeis new.graph/lpg.Graph:RemoveEdge,RemoveAllEdgesFrom,RemoveEdgeInstanceand
RemoveEdgeInstanceByHandlenow returnerror;RemoveEdgeByHandlenow returns
(bool, error).SetEdgeRelTypeAtSlotByIDandAddEdgeRelTypeOverflowByIDkeep
their signatures and can now return the refusal.graph/lpg.WriteView:Revive,RemoveEdgeInstanceand
RemoveEdgeInstanceByHandlenow returnerror.RemoveNodenow returns
(bool, error): the error is the refusal behind afalse, the transaction's
recorded conflict inside a transaction and the wrappedErrDirectWriteConflict
over the zeroWriteTx. Over the zeroWriteTx,RemoveEdge,RemoveEdgeByHandle
andRemoveAllEdgesFromreportfalsefor the refusal.graph/lpg.Graph.ApplyDurable, new in this release, takes acontext.Contextas its
first argument.
It bounds the waits behind conflicting commits; a cancelled call returns an error
wrapping the context's error, with nothing applied and nothing durable.store/txn.Tx.CommitCtx(ctx)is new:Commitwith its waits behind conflicting
commits bounded byctx; a cancellation before the apply holds its claims writes
nothing to the WAL.CommitisCommitCtx(context.Background()).
ErrCommittedNotAppliedis deprecated: no function returns it any longer.graph/mvcc:ImplicitTxBit,IsImplicitTx,Clock.NextImplicitTxIDand
Tx.Implicitare new.graph/adjlist.AdjList:WithdrawTxand the diagnosticCheckInvariantsare new
(rmp #2965).graph/mvcc:TxState.NoteAdjacency,TxState.AdjacencyWrites,TxState.Touch,
TxState.TouchedandTx.NoteAdjacencyare new (rmp #2965).
- Direct present-state reads return the newest COMMITTED state (rmp #2965, round 5).
graph/lpg.Graph's node property and label accessors (GetNodeProperty,
NodeProperties,NodePropertiesByID,NodePropertyByID,NodePropertiesByIDFunc,
HasNodeLabel,HasNodeLabelByID,NodeLabels,NodeLabelsByID,
ForEachNodeLabelByID), its edge label and property accessors (EdgeLabels,
EdgeLabelsByID,ForEachEdgeLabelByID,HasEdgeLabel,EdgeLabelsAt,
EdgeLabelsByHandle,EdgeLabelsByHandleID,HasEdgeHandleLabelRecordByID,
ForEachSlotRelTypeByID,ForEachPairSlotRelTypeByID,ForEachPairOverflowRelTypeByID,
EdgeProperties,EdgePropertiesByID,GetEdgeProperty,EdgeHasProperty,
ForEachEdgeProperty,ForEachEdgePropertyByID,EdgePropertiesAt,
EdgePropertiesByHandle,EdgePropertiesByHandleID,EdgePropertyByHandle), its
topology accessors (EdgeWeight,FirstEdgeHandle,HasEdgeHandle,
AppendEdgeHandles,WalkEdgeHandles,OutDegree,OutDegreeByID,
OutDegreeByType,OutDegreeBounded…andOutDegreeMatchingBoundedByID), and
graph/adjlist.AdjList'sHasEdge,Neighbours,OutDegree,OutDegreeByID,
OutDegreeByType,OutDegreeFunc,OutDegreeFuncBounded,OutDegreeFuncBoundedByID,
InNeighbourIDsandInNeighboursused to read the stored value, which carries every
uncommitted write: a durable commit applied but not yet fsynced was visible through
them and was withdrawn again if the fsync failed. They now step back over every
uncommitted version, as an implicit transaction reads; a read whose newest version is
committed allocates nothing. A transaction reads its own writes through
Graph.WriterViewOf/Graph.Writer; the…AsOfforms with a nil snapshot still read
the stored value.Graph.HasEdgeHandleAsOf,Graph.AppendEdgeHandlesAsOf,
adjlist.AdjList.InNeighbourIDsStoredandlpg.WriteTx.Versionsare new.
Graph.IsTombstoned,TombstonedIDs,TombstoneCount,LiveOrderandLiveNodeFilter
now read the newest committed state as well, through the node-life records; the stored
bitmap readers are kept asIsTombstonedStored,TombstonedIDsStored,
TombstoneCountStored,LiveOrderStoredandLiveNodeFilterStored. Not changed, and
documented as stored-state primitives: the label index returned byNodeIndex, the
aggregate counts,AdjList.Size/Orderand the rawAdjList.LoadEntry*loaders, which
have no versioned form. - The WAL records effects, not requests (rmp #2965, round 5).
store/txn.Tx.Commit
and the Cypher engine's durable write path log only the operations that changed
something: anAddNodeof a live node, a label already present, a property set to the
value it holds, or a removal of something absent writes no WAL frame, and a commit in
which nothing took effect writes nothing and mints no sequence. Such an operation holds
no claim, so another commit could change the object and log first, and recovery then
replayed the no-op after it, where it was no longer one: recovery rebuilt a node the
acknowledged state had deleted and a label it had removed. This change alone alters
neither the record format nor the recovery contract (the WAL v2 format change above is
separate). The four schema-DDL ops are still always logged.
Added
store.Open/store.OpenCtx: the composed open (rmp #2523). One call runs the
reopen sequence every embedder used to hand-write: recovery, the clean gate,
wal.Open, and the transactional store built from the recovery result. The returned
*store.Opened[N, W]embeds*store.DB(with the store's commit lock already wired as
the close quiesce) and exposesStore(),Graph(),Recovery()andWAL(), so the
recovered transaction sequence, the replay op cap, the codecs and the schema cannot be
dropped by omission. A recovery that is not clean, including the nil-error
recovery.ErrCommittedTxnCorruptOpoutcome, is refused with a
*store.UncleanRecoveryErrorwrappingstore.ErrUncleanRecoveryand the recovery's
TailErr; nothing is opened for append. WithOptions.AllowUncleansuch a directory
opens read-only instead: the committed prefix andRecovery()are readable,
ReadOnly()reports it, no WAL writer is opened (no lock, no byte written), and every
commit that would write, including every Cypher write, fails with
store.ErrReadOnlyStore(=txn.ErrReadOnlyStore).txn.NewReadOnlyStoreand
txn.Store.ReadOnlyare the store side of that mode.cypher.NewEngineWithOpenedbuilds the engine
over an opened store and re-registers the recovered schema. Non-breaking: the low-level
recovery.Open,Result.NewStoreandstore.Neware unchanged. Examples 24 and 25 now
reopen throughstore.OpenCtx.store/txn.(*Store).Close(rmp #3021). Ends the store's use of its WAL for node-id
reservations: it stops the reservation prefetch goroutine and appends the exact per-shard
high-water marks (aNextIDsExactcontrol record), so a clean restart wastes no node id. It
does not close the WAL and does not refuse later transactions. Call it with writers quiesced,
before the WAL is closed;store.Opened's close does so under the commit lock. Without it, the
next recovery falls back to the logged reservations and wastes at most one batch per shard. It
is idempotent and safe for concurrent use.- WAL v2 API (
store/wal).OpenWithOptions,OpenFSWithOptionsandOptions
(segment size, fsync latency injection),OpenLog/OpenLogFSandLogfor reading a
segmented log,FrameSource,Control,ControlPath,SegmentDir,SegmentPath,
PrefixTruncatedMarkerPath,WritePrefixMarker/WritePrefixMarkerFS, the control-record
codecs (AppendReserveIDs,DecodeReserveIDs,AppendNextIDsExact,DecodeNextIDsExact,
EncodeLegacySeal,DecodeLegacySeal) and the errors named above.
wal.NewSyncLatency/store.Options.SyncLatencyinject a seeded fsync latency for tests. - Node-id reservation API.
graph.IDReserver,graph.MapperWatermark,
graph.MapperShardsand theMappermethodsPlaceUnborn,Unplace,PublishReservation,
ReservedLimit(s),RestoreNext,SetIDReserverandWatermark;lpg.Graph.ReserveNodeID,
PlaceNodeID,UnplaceNodeID,NodeBornAsOfandSettleNeverBorn;txn.Tx.AttachWriteTx;
snapshot.WriteNodeIDs/ReadNodeIDsandsnapshot.NodeIDsFile. - Cypher:
UNIONandUNION ALLinsideEXISTS { }andCOUNT { }(rmp #2627).EXISTS
evaluates branches in order and stops at the first match;COUNTsums branch counts for
UNION ALLand de-duplicates forUNION. Column compatibility and mixing are checked as at
statement level.ast.QueryBranchesis new. - Cypher:
PROFILEof writing statements (rmp #2626, #2790).PROFILEofCREATE,SET,
REMOVE,DELETE,DETACH DELETEandMERGEnow runs; the write applies exactly once, with
the same counters as without the prefix.Engine.Runstill refuses a writingPROFILE, as it
refuses any write. - Fail-stop sentinels.
cypher.ErrEngineFailStopped(a commit-time delivery panicked and
the engine refuses further work) andgraph/index.ErrIndexStateUndefined(an index delivery
was cut short; a reopen rebuilds the indexes).lpg.ErrIndexedRawWriterefuses a raw write
while a secondary index is registered.txn.ErrValueTooLongseparates an over-long property
value from othertxn.ErrFieldTooLongcauses, anderrors.Is(err, txn.ErrFieldTooLong)still
holds for it (rmp #2942). - Byte-reproducible output, fault-injectable bulk import (rmp #2518, #2519, #2521, #2534).
Snapshot, JSONL and GraphML output sort property keys and labels, so identical input yields
identical bytes.bulkimport.PublishFSandbulkimport.ImportIntoFSpublish over a
caller-supplied filesystem.
Fixed
-
A degree read no longer hides a neighbour removed after its snapshot (rmp #2969).
The typed, matching and bounded out-degree walkers filtered neighbours on the stored
tombstone bitmap even when reading as of a snapshot, so a removal committed after the
snapshot, or one not committed at all, lowered the count. Liveness is now resolved at
the read position. -
The durable engine no longer writes the per-CREATE-ordinal edge store (rmp #2968).
It had no WAL frame, so it was lost on recovery and kept a deleted relationship's
entry in memory. Every reader of it is a fallback that the WAL-described handle store
answers for every relationship the engine creates, so the store is retired on the
durable path; the live engine now equals the recovered one. -
WAL order now equals publication order (rmp #2965, round 5). The transaction
sequence of a durable commit is minted inside the WAL writer's append critical section,
so sequence order — the order transactions become visible — is the order their records
appear in the file. It used to be minted before the append, so two committers could
take sequences in one order and append in the other. -
A write waiting for a durable commit parks instead of polling (rmp #2965, round 5).
A store commit or direct write refused by another durable commit's uncommitted version
used to abort, back off for at most a few hundred microseconds and rerun until the 1 s
budget ran out, so whoever reran first won: at 32 committers on one node the commit
p99 was 1.000 s, with committers refused at the budget. It now parks on that commit's
end and is woken in arrival order, holding nothing while it waits; the commit releases
its waiters on every exit, including a failed fsync and a panic. Measured on the same
probe: zero refusals and a p99 of 136 ms at 32 committers. Commits of one object still
serialise across the fsync, which is inherent to claiming before the commit record. -
An aborted transaction left its adjacency writes applied (rmp #2965). A write
transaction that added or removed an edge and was then aborted by a conflict on
another store withdrew its node and edge-side writes but not the adjacency entry,
when no undo log ran:Graph.ApplyVersionedas the durable store's in-memory apply
runs it,Graph.ApplyAtomicallyTx, and an explicit transaction ended without the
Cypher engine's undo. The added edge stayed present and the removed edge stayed
removed for every present-time reader, in the reverse index and the edge count, and
the next write on the node built on it. Every abort now restores the entries the
transaction published to their pre-images before it marks the transaction aborted.
It visits only the transaction's own adjacency write set, and restores each entry
as a copy whose columns carry no spare capacity, so the next append cannot write
into memory a reader of the aborted entry still holds. -
An explicit transaction could build on another's uncommitted relationships (rmp
#2966). A relationship-property or relationship-type write of an explicit
transaction rebuilt the source node's adjacency entry without claiming the node,
so it carried another transaction's uncommitted relationships into its own
commit: through the Cypher engine, a relationship a doomed transaction rolled back
was committed by the other. Every such write now claims the node. Behaviour
change: two transactions writing properties or types on relationships out of
the same node now conflict, and the second returns a retryable serialization
conflict, as two transactions creating relationships from one node already did. -
A durable store commit could publish another transaction's uncommitted edge
(rmp #2965 audit). The store's edge apply (Graph.AddEdgeHIfAbsentinside
txn.Tx.Commit) claimed nothing, and the adjacency let an explicit transaction
write over another explicit transaction's uncommitted entry. The commit's entry
then carried that transaction's arc: a Cypher reader saw it while the transaction
was open, it stayed in the graph after the transaction rolled back with no WAL
record of it, and an undirected edge was left in one endpoint only. The
adjacency now refuses every write over another transaction's uncommitted entry,
andAddEdgeHIfAbsentclaims both endpoints asAddEdgedoes. -
AdjList.Compactdropped the version chain of every entry it trimmed (rmp
#2965 audit). A snapshot reader then saw uncommitted arcs, and an aborted
transaction's arc on a trimmed entry survived its rollback. The trimmed entry now
keeps the chain. -
A durable store commit is refused before its WAL record, never after it (rmp
#2965 audit).txn.Tx.Commitfsynced its record and then applied it in memory,
where a concurrent direct write or an open explicit transaction could refuse it:
measured 85 of 268 commits returnedtxn.ErrCommittedNotAppliedin one second
with four direct writers on the same node. Commit now applies first, as an
uncommitted transaction whose versions claim what it writes, then appends and
fsyncs, then publishes (lpg.Graph.ApplyDurable, new). Behaviour change:
Commitno longer returnsErrCommittedNotApplied. A conflict, a validator
refusal oradjlist.ErrShardFullis returned with nothing written to the WAL;
a conflict wrapsmvcc.ErrSerializationConflictand is retryable. A conflict
with a direct write or another store commit is waited out first, within 1 s;
one with an explicit transaction is returned at once. Writes to the same object
now serialise across the fsync: a direct write on a node a store commit is
writing waits for that commit's fsync.mvcc.BoundedTxBit,mvcc.IsBoundedTx
andmvcc.Clock.NextBoundedTxIDare new. -
AdjList.RemoveAllEdgesFromreleased its shard locks by hand
(rmp #2965 audit). A panic under them would have left the shards locked and
deadlocked the aborting transaction's withdrawal; they are now released by a
deferred call. -
A raw adjacency write made during an exclusive bracket was lost with it (rmp
#2967). A write made onGraph.AdjList()with no transaction while
ApplyAtomically,ApplyAtomicallyTxorLockBarrierCtxwas open took the
bracket's commit record and could displace the bracket's uncommitted entries;
when the bracket aborted, the acknowledged write became invisible to every
snapshot reader. Behaviour change: such a write is now its own
single-operation transaction. It commits at its own instant and survives the
bracket, and over an entry the bracket — or any other transaction — has written
and not committed it returns a retryable*mvcc.Conflictand changes nothing. -
An abort no longer scans every node-life record and adjacency claim (rmp
#2965). Each abort ran both scans whatever it had written; they now run only
after an abort that wrote to those stores. A refused direct append allocates no
commit record, so it needs no abort. -
A direct write that panics no longer leaves its transaction in flight (rmp
#2947). The panic still reaches the caller; the implicit transaction is aborted
on the way out. -
A deleted node could stay in a label index after a concurrent label change (rmp
#2947). A peer's label removal on a node being deleted replaced the deletion's
pending index removal, so the peer's rollback withdrew both; and a label set while the
deletion was between its index strip and its tombstone flip was never retired. In
both cases every present-time scan of the label kept returning the deleted node. -
A revival refused by a conflict still revived the node (rmp #2947). The tombstone
was cleared after the existence claim had been refused, undoing another
transaction's pending delete in the present state. -
The overflow relationship-type gate could read zero while overflow types existed
(rmp #2947). Withdrawing an aborted transaction's overflow change restored the list
without adjusting the count, so a later read could skip a pair's overflow types. -
A deleted node could stay in a label index for good, and a revived one could drop
out of it (rmp #2964). Re-asserting a label on a node while its removal was in
flight withdrew the removal's pending index entry, so every present-time label scan
kept returning the deleted node. Reviving a node did not withdraw its earlier
removal, so the next reclamation took the restored entry out. A removal is now
withdrawn only by the node's revival. -
MVCCStats.NodeLifeRecordsover-reported, and the node-life gate never returned to
zero, after a node was revived or removed twice (rmp #2962). A second birth or death
of the same node overwrites its record but was counted as a new one. -
A relationship type over 65535 bytes was acknowledged on a WAL-backed store and
recovered with no type (rmp #2956). The WAL adapter discarded the staging refusal, so
the statement and its commit succeeded while nothing reached the log. The statement is now
refused before commit withNeo.ClientError.Schema.TokenLengthErrorover Bolt, and nothing
is written.
The entries below are one line per commit and carry its subject's meaning; the commit body
holds the detail and the regression test.
Durability and recovery
- A WAL-v2 recovery that read no frame from the first retained segment lost the prev-link, so
the next commit was acknowledged and the following open failed; it is now seeded on every
exit (6725348d). - A bulk-import publish now fsyncs every directory it creates, so an acknowledged publish
survives a host crash (rmp #2970,93ec8926). ApplyDurablehonours its context while it waits at the visibility gate (rmp #2985,
97732d9c).- A store commit strips a removed node through a view taken after its claim (
f6f78930). - A checkpoint capture is no longer refused when a rolled-back creation withdraws a birth
record after the capture instant (828f1661).
MVCC, indexes and constraints
- Commits, indexes and constraints stay consistent under concurrency; index and constraint
state is read at the reader's snapshot (bfaf3ed6,d1a9cfe7). - A displaced adjacency or constraint commit stays visible after an abort; node visibility is
stable inside one snapshot (32efbe73,c3764022,676a6b94). - Hash and label indexes stay in step with no-op writes under churn; no-op writes take no
conflict stamps; a by-handle removal of an absent key writes nothing (8999e4b4,
19443683,ac2d0203). - Writes and removals on a node whose creation is aborting are refused as a retryable
conflict (1196fe2b). - An admitted no-op in a bounded transaction is decided through an unpinned view
(569925f3). - The node-life gate is raised under the shard lock that publishes the record (
bc9b6cad). - A cancelled gate request is withdrawn instead of left queued (
3e8e2103). - A
UNIQUEreservation whose release predates the constraint is accepted (42fef7f2). DELETEis decided from the transaction snapshot, undirected mirror handles are cleared, and
a raw write under a registered index is refused (cd27c23f).Mapper.Walkcallbacks no longer re-enter the mapper (dbef3823,b2260658).
Cypher engine
- Rollback undoes parallel edges by instance handle and captures undirected removal
pre-images in the creation direction (47334e3b,787e674a). - A rolled-back explicit transaction and a failed autocommit statement end as aborts
(e68915bc,2a5ad3c5). - DDL and the autocommit write's schema-gate wait honour the caller's context; cancellation is
honoured before plan initialisation (9fac2b0a,a3aa78e4,f14fb3bf). - Deleting a relationship a peer removed records a conflict;
DETACH DELETEremoves every
parallel incoming arc; a concurrent autocommitMERGEmatches theUNIQUEwinner
(8f933554,9d0d315e,fe8864b0). - Explicit-statement counters are reported, removal side effects are gated, and a property
removal is recorded and counted only when it took effect (3f545e44,6b5525dd). - Write-clause property maps are read through the transaction snapshot; a bound relationship
resolves inside write-path maps; every map-write value is stored or refused,MERGEactions
are ordered and theDELETEguard is deferred (c8e27ba8,5b183a3c,25999b58). - Node keys are minted so that they never collide with keys the target graph already holds
(cd97fdf5). - Index nested-loop coverage is proved at the reader's snapshot; the label of its seek hits is
guarded (4fc83efd,b60c31c1). - The key-set seek declines a non-string key, and a mismatched seek-set key returns a typed
error (e8a1c74d,03301f27). - A list property containing
nullis refused asInvalidPropertyType;ErrFieldTooLongis
split by field kind with its own Bolt code (7e74e34f,7083ffcc). - A plan compiled before a DDL cleared the cache is not cached (
5b5b4477). - Boundary outputs are marked scalar so an integer is never decoded as a node; binding metadata
is re-keyed at projection boundaries and relationship uniqueness is scoped per pattern
(73d3a4f0,0a2b7d18). - Every relationship variable is addressed by its stable handle; named-path hops are rendered
from the bound edge handle (0846713a,09a0b34c). - Pattern predicates: relationship uniqueness, bound relationship variables and relationship
property maps, exact variable-length hops, bound first hops, type conflicts and outer-row
property maps (b2260658,1075ceaa,bd5f7fbe,dfa9187f,79a50e41). - Variable-length lists across
WITH, identity-based no-repeat, per-edge properties after
WITHandsize()of variable-length lists (23b5b860,4af8739c). - Same-hop property maps in pattern comprehensions are evaluated per candidate; property access
and labels are allowed after a list subscript (1f6aec45,65a38f1a). - Per-variable build facts are scoped like the schema (
3cef9714). - Parser:
reduce()is a grammar rule and parses inside subscripts and slices; hand-patched
rules set their invoking states; parenthesised commas stay inside a property-map value;
STARTS WITHandENDS WITHparse in a query part afterWITH(d18650bb,789732a5,
9080e5b8,0a210dff). - Temporal values order between Path and String per CIP2016-06-14 (
208a3ec7).
Other components
graph/ioexporter name tables are bounded against concurrently created nodes
(0e0e126a).- Metric shards are chosen from a mixed stack address; under
-raceonly 16 of 32 shards were
reachable (rmp #3065,0f0ab6f4).
Performance
Figures are development-time measurements recorded by the commit that shipped each change,
against that commit's own control. They are not additive and no whole-release delta is claimed.
- Index seek on absent values (rmp #3061, #3062). A btree equality, prefix, range or
intersected conjunct whose value the index does not hold now seeks instead of scanning the
label: absent equality at 20 000 nodes 1.32 ms → 3.65 µs. A multi-key hash seek whose keys
are all absent seeks too: 20 000 nodes 9.3 ms → 4.22 µs. Present values are unchanged
(3a715fa1,720545e5). - Sole-leaf seek on a written property (rmp #3056). A seek that is the plan's only leaf,
underSET/REMOVEoperators that initialise their child once, keeps the index when the
statement writes the property it seeks:BenchmarkWriteScalingUniquesec/op −52.9 % at 1
writer to −83.0 % at 32 (7947c3b9). String seek keys in the snapshot fallback match without
allocating: allocs/op 76–79 at 1–32 writers, was 78–490 (rmp #3057,5c8511ee). - Planning over label churn (rmp #3010, #3011). Plan-choice sites use a bounded label count
above 4 096 unreclaimed records, corrections are cancellable and memoised per statement:
high-churn count −72 %, cancelled-statement lag 685 ms → 12 ms under-race(2df8273b). - MVCC housekeeping. A long snapshot's visibility memo prunes resolved transactions: B/op
−59.5 % over 200 000 operations (rmp #3026,5150034e). A small vacuum pass waits for the
next wake: direct-write benchmarks −23.5 % (rmp #3060,a859dddd). A churned label is counted
without cloning its bitmap, and a small emptied stamp map is kept across vacuums
(2f64a880,11305259). - Bolt:
Server.Transactionssorts in n log n: 1 024 open transactions 2 536 µs → 92 µs
(rmp #2562,e7d59dc3). - Sprint 363 waste hunt. Unbracketed adjacency writes are stored in place unless a snapshot
pinned the version; forward expansion inside a write transaction reads live adjacency;
per-slot type resolution, per-row expression evaluation and the row passed to a childNext
stop allocating; pattern predicates reject non-matching neighbours before the label lookup
(4f733f9e,fa6fc459,a1c7bb9f,f6b96b15,5de6174b,120bff89). The sprint's merge
records elevated-pass CPU −61.6 % and allocation about −62 % across the examples; the
methodology is in docs/campaign-waste-hunt-2026-09-24.md.
Removed
store/wal.(*Writer).TruncatePrefix,store/wal.ErrPrefixTruncateUnsupported,
store/recovery.Result.WALTailOffsetandstore/snapshot.ErrCaptureNotQuiesced; see
Changed — BREAKING: exported API.
Deprecated
store/txn.ErrCommittedNotApplied: no function returns it any longer (rmp #2965).
Further reading
- Narrative release notes:
release-notes/v0.16.0.md - Measured benchmark report:
docs/benchmarks/v0.16.0.md - Changelog file at this tag:
CHANGELOG.md - Commits in this release: https://github.com/FlavioCFOliveira/GoGraph/commits/v0.16.0
Reproducibility: built from commit eb72b11 with Go 1.26.