Skip to content

Releases

v0.17.2

Choose a tag to compare

@github-actions github-actions released this 13 Oct 23:11

GoGraph v0.17.2 — see CHANGELOG.md for the per-release entry.

0.17.2 — 2026-10-13

10 commits — 8 non-merge commits and 2 merges — counted at 700eaed6, the tip of
release/0.17.2: the back-merge of v0.17.1 into develop (2b96446d); sprint 369, Bounded
rollback latency and memory for cut write statements
(5 commits, merged at 18ec0dc7); the
release-preparation commit (b87ec901); and two stabilisation commits on the release branch, a
test-only fix to a gauge sampler (3b03915a, rmp #3126) and the constraint fix below
(700eaed6). 17 files changed, 1 262 insertions, 85 deletions
(git diff --shortstat v0.17.1..700eaed6).

This is a PATCH release: no exported API change, no WAL or on-disk format change. The only
non-test Go files changed are four files of package graph/lpg (lpg.go,
mvcc_abort_reclaim.go, mvcc_abort_release.go, mvcc_abort_sides.go) and four of package
cypher (api.go, exectx.go, undo_record.go, exec/constraints.go). Every declaration they
add or change is unexported: in graph/lpg, abortWithdrawsCreation, tombstoneAbortedBatch,
idBatchBitmap, countSideStores, and the lifeWithdrawal field sizeHint with its methods
tombstone and revive; in cypher, the Engine test seams beginTxGatesHookForTest and
delPropProbedHookForTest, the releasesOwed fields of Result and ExplicitTx,
Result.applyOwedReleases, and the signature of recordDelNodeProperty. No file under store/
changed. go.mod and go.sum are unchanged from v0.17.1.

Fixed

  • A write statement cut by its context deadline no longer takes many times its deadline to
    roll back, nor multiplies its peak heap while doing so (rmp #3122, #3123, b1e9e21f).

    Present in v0.17.1 and earlier. The rollback deleted every node the statement had created
    through the ordinary delete path, which copied the whole copy-on-write tombstone bitmap once per
    node and recorded bookkeeping the abort then withdrew: the rollback was quadratic. A
    MATCH (a), (b), (c) CREATE () over 600 nodes cut at 1 s returned 8.6 to 9.2 s after its deadline
    and peaked at about 1.7 GB of heap; cut at 5 s it returned 64 s late and peaked at 7.2 GB. The
    abort now withdraws the nodes the statement created and flips their tombstones in batches, with
    the same end state. The same statement now returns about 1.4 s after a 1 s deadline with a peak
    of about 0.75 GB, and about 7.1 s after a 5 s deadline with a peak of about 3 GB. The rollback is
    now linear, at about 1 µs per created node. The deadline itself was always noticed within
    microseconds.
  • A REMOVE racing a peer's rolled-back removal can no longer commit a node without a
    property its NOT NULL constraint requires (rmp #3127, 700eaed6).
    Present in v0.17.1 and
    earlier. The removal decided whether to check the node at commit from a probe of the present
    state; when that probe read a peer's uncommitted removal that the peer then rolled back, the
    removal still deleted the committed value but skipped the commit-time check. The check now
    follows the removal's effect. A second, narrower window is closed in the same commit: an
    explicit transaction whose BeginTx straddled a constraint registration between its catalogue
    reads skipped both the commit-time NOT NULL check and the straddler validation; BeginTx now
    reads the constraint generation first.
  • Readers can no longer briefly see two holders of a UNIQUE value (rmp #3128,
    700eaed6).
    Present in v0.17.1 and earlier. A transaction that freed a UNIQUE value
    released it at its commit decision, before it was visible, so a peer could take the value and
    publish first; a snapshot between the two publications held the value twice. The release is now
    applied after publication, on the autocommit and the explicit-transaction path. Behaviour
    change:
    a concurrent write of a just-freed UNIQUE value, inside the window between the
    freeing transaction's commit decision and its visibility, is now refused with a constraint
    violation; a retry succeeds.
    The concurrency test that exposes both defects, TestConstraintDDLvsDML_NoViolationAdmitted,
    failed 9 runs in 200 on v0.17.1 and 3 in 100 at 3b03915a; after the fix it passed 1 000 of
    1 000 runs, and 200 of 200 under -race.

Performance

  • The abort path allocates about a third less per created node (rmp #3125, 2885d7d3):
    pre-sized withdrawal lists, one bitmap Or/AndNot per batch of at least 1024 ids instead of
    per-id updates, and no duplicate unborn-mark pass. Rolling back a 5 s cut CREATE allocates
    197 B per id instead of 307. A large DELETE rollback was measured linear and is unchanged
    (rmp #3124).

Further reading


Reproducibility: built from commit 3d5fd8c with Go 1.26.

v0.17.1

Choose a tag to compare

@github-actions github-actions released this 13 Oct 13:12

GoGraph v0.17.1 — see CHANGELOG.md for the per-release entry.

0.17.1 — 2026-10-13

4 commits — 2 non-merge commits and 2 merges — counted at 4743de3d, the develop tip
before release preparation: the back-merge of v0.17.0 into develop (81ed1eae) and sprint
368, Rollback memory reclamation in graph.Mapper (2 commits, merged at 4743de3d). 18 files
changed, 1 870 insertions, 40 deletions (git diff --shortstat v0.17.0..4743de3d).

This is a PATCH release: no exported API change, no WAL or on-disk format change. Measured
by parsing every non-test Go file outside internal/, examples/, cmd/ and any gen/, tck/
or testdata/ directory at both v0.17.0 and 4743de3d, and diffing the rendered declarations
— a superset of the API surface docs/semver.md defines: the two renderings
are identical. The only new declarations are in internal/synthkey, which the module does not
export. No file under store/ changed. go.mod and go.sum are unchanged from v0.17.0.

Fixed

  • A rolled-back write statement no longer leaves its node keys interned in graph.Mapper
    (rmp #3119, #3120, 78039599).
    Present in v0.15.0, v0.16.0 and v0.17.0; fixed in
    v0.17.1
    , so embedders pinned to an earlier version should move their pin. When a
    transaction that created nodes aborted as a whole — an autocommit statement cut by its context
    deadline, a failed or refused statement, or an explicit transaction rolled back — the anonymous
    keys it minted for CREATE () stayed in the Mapper, together with the transaction's write set,
    adjacency-version entries and unborn marks. A cut MATCH (a), (b), (c) CREATE () over 600
    nodes left the live heap at 1847.5 MB after runtime.GC(), against 2.3 MB before the
    statement. The abort now releases every never-born node created under a minted key: the key no
    longer resolves, its id becomes a hole, and the Mapper gives the memory back; the same scenario
    now leaves 2.5 MB. Committed ids and elementId() are unchanged, ids are never issued twice,
    and a statement that fails inside a transaction that later commits keeps its bindings, as
    before.
    (*graph.Mapper).MaxNodeID no longer counts holes trimmed from the end of a shard.
  • New keys interned after a large rollback no longer re-create the released id range in memory
    (rmp #3121, 54a2e218).
    Ids are never reissued, so the first keys after a rollback land past
    the released range; that range was materialised again as holes, about 16 B per released id
    (1000 creates after the cut above: 124.3 MB live). A shard now keeps such a run of holes at its
    end in no memory, with the new keys held in small pages allocated on demand; the same 1000
    creates leave 2.9 MB. Holes in the middle of a shard — nodes committed in the same shard while a
    large statement was rolled back — still cost about 16 B per id.

Performance

Figures are development-time measurements recorded in the rmp task log of sprint 368 (#3119 to
#3121) on this code. They are not additive.

  • The dense Mapper paths did not regress: Resolve +0.9 % (p=0.089, not significant),
    Walk −3.5 % (benchstat, 10 interleaved rounds; 54a2e218).
  • Cost recorded: releasing the keys of an aborted transaction adds about 1.5 s to its
    rollback per about 1.6 M released ids.

Further reading


Reproducibility: built from commit 7e71665 with Go 1.26.

v0.17.0

Choose a tag to compare

@github-actions github-actions released this 13 Oct 08:52

GoGraph v0.17.0 — see CHANGELOG.md for the per-release entry.

[0.17.0] — 2026-10-13

53 commits — 50 non-merge commits and 3 merges — counted at e91d42b2, the develop tip
before release preparation. Two sprints delivered this window: 366, Drop undirected
relations and simple graphs
(6 commits, merged at c794a1ed), and 367, Performance and
Efficiency Laboratory 261009
(41 commits, merged at 4d1947e8); 3 documentation commits follow
the last merge. 1 560 files changed, 19 571 insertions, 9 450 deletions
(git diff --shortstat v0.16.0..e91d42b2).

This release breaks the exported API and changes the on-disk format. Undirected graphs and
simple (non-multigraph) graphs are no longer supported: every graph is a directed multigraph, as
in Neo4j and Memgraph (rmp #3071 to #3074). Measured by parsing every non-test Go file outside
internal/, examples/, cmd/ and any gen/, tck/ or testdata/ directory at both
v0.16.0 and e91d42b2, and diffing the rendered declarations — a superset of the API
surface docs/semver.md defines: 5 152 exported declarations before, 5 162
after. 14 were removed, 1 changed signature and 24 were added; one of the 24,
cypher/exec.(*ColumnarFilter).Close, redeclares a method the type already had through
embedding. Under
docs/semver.md a 0.y.z release absorbs breaking changes in the MINOR digit.
go.mod and go.sum are unchanged from v0.16.0.

Changed — BREAKING: directed multigraph only

  • Every graph is a directed multigraph. graph/adjlist.AdjList stores each edge as one
    directed relationship, and every AddEdge appends a new edge with its own handle, so parallel
    edges and self-loops are always kept. The zero value adjlist.Config{}, which built an
    undirected simple graph, now builds a directed multigraph. A repeated AddEdge between the same
    ordered pair now adds a parallel edge (fec95062).
  • Removed exported API:
    • graph/adjlist: Config.Directed, Config.Multigraph, (*AdjList).Directed() and
      (*AdjList).Multigraph() (fec95062).
    • graph/io/csv: Options.Directed and Options.Multigraph; DefaultOptions no longer sets
      Directed (fec95062).
    • store/bulk: Options.Directed and Options.Multigraph (fec95062).
    • store/bulkimport: Options.Directed and Options.Multigraph (fec95062).
    • store/csrfile: FixtureSpec.Multigraph (fec95062).
    • store/snapshot: GraphConfig.Directed and GraphConfig.Multigraph (fec95062).
    • cypher: ErrParallelEdgeInSimpleGraph; every CREATE adds a relationship (bed4de53).
    • cmd/gograph-import: the -undirected and -simple flags (fec95062).
  • Re-signed: graph/adjlist.Writer.AppendEdge returns error instead of (bool, error); the
    boolean reported a simple graph's skipped duplicate (fec95062).
  • Interchange formats (fec95062). The GraphML readers (ReadInto, ReadWithProps and their
    variants) read each <edge> as one directed relationship from source to target, whatever
    the <graph> edgedefault attribute declares, "undirected" included. The GraphML writers
    always emit edgedefault="directed", and the DOT writer always emits a digraph.
  • Upgrading code:
    • Delete Directed and Multigraph from every adjlist.Config, csv.Options,
      bulk.Options, bulkimport.Options and csrfile.FixtureSpec literal, and -undirected and
      -simple from every gograph-import invocation.
    • For undirected analytics, build the CSR and read its symmetric projection,
      c.BuildSymmetric() (see Added).
    • For undirected Cypher semantics, write undirected patterns: MATCH (a)-[r]-(b) matches a
      relationship in either direction, and MERGE (a)-[r:T]-(b) matches either direction and,
      when none exists, creates one from a to b. CREATE refuses an undirected relationship.
    • Code that relied on a simple graph to de-duplicate a repeated AddEdge must not repeat the
      insertion.

Changed — BREAKING: on-disk format

  • store/snapshot.ManifestVersion is 5 (was 4) and is stamped on every manifest this build
    writes
    , including the CSR-only and mapper-less ones previously stamped 1 and 2. A version-5
    graph_config carries neither "directed" nor "multigraph". Versions 1 to 4 still load.
    v0.16.0 and earlier builds refuse a version-5 manifest with snapshot.ErrManifestUnsupported
    (fec95062).
  • Legacy stores are migrated by the first clean recovery (fec95062). For a store whose
    snapshot manifest declares "directed": false or "multigraph": false, recovery replays the
    WAL above the snapshot with the old engine's semantics, then:
    • Undirected: folds each edge to one relationship oriented from the lower node id to the
      higher; a self-loop is kept once. When the two directions of an edge hold different values
      for the same property key, or different relationship types, recovery returns an error
      wrapping recovery.ErrLegacyMirrorConflict that names the conflicting edges, and the
      directory is left unchanged. A value held in one direction only is kept.
    • Simple: skips each duplicate insertion the old engine skipped, keeping its edge handle
      reserved.
    • Before Open returns, it publishes a current-format snapshot and folds the WAL under it. This
      needs write access to the directory, the operating-system filesystem
      (recovery.ErrLegacyMigrationUnsupportedFS otherwise) and a non-nil Options.Codec
      (recovery.ErrLegacyMigrationNeedsCodec otherwise). An unclean recovery does not rewrite the
      directory. A crash during the rewrite leaves either the legacy store, which the next open
      migrates again, or the migrated one.
    • A migrated store cannot be opened by v0.16.0 or any earlier build. Take a copy of the store
      directory before the first open if a downgrade must remain possible.
  • The legacy shape is read from the snapshot manifest only. A store directory with no
    snapshot/manifest.json records no graph shape, so recovery replays its WAL with
    directed-multigraph semantics and migrates nothing (store/recovery/recovery.go,
    legacyReplayOf).
  • A directed multigraph store written by v0.16.0 opens without migration. Its version-4
    manifest still loads; the first checkpoint this build takes writes a version-5 manifest, after
    which v0.16.0 refuses the directory. The WAL format is unchanged in this release.

Added

  • graph/csr.(*CSR).BuildSymmetric returns the undirected projection of a CSR: each non-loop
    arc yields itself and its mirror with the same weight and handle, a self-loop is kept once, and
    parallel arcs and reciprocal pairs are never merged (11b2ddd1).
  • store/snapshot: LegacyShape (with Any), LoadedSnapshot.Legacy,
    GraphConfig.UnmarshalJSON, VerifySnapshotReadable, SelfSufficient and
    ErrPublishedLegacyShape (fec95062).
  • store/recovery: ErrLegacyMirrorConflict, ErrLegacyMigrationUnsupportedFS and
    ErrLegacyMigrationNeedsCodec (fec95062).
  • graph/csr.BuildSymmetricFromAdjList builds the symmetric projection directly from an
    adjacency list, without the intermediate directed CSR (rmp #3078, e7b12a60).
  • graph/csr.(*CSR).KnownSymmetric reports in O(1) whether a CSR was returned by
    BuildSymmetric or BuildSymmetricFromAdjList (rmp #3078, cfd64290).
  • search/centrality.PageRankBudget, NewPageRankBudget, (*PageRankBudget).Capacity,
    (*PageRankBudget).InUse and the field PageRankOptions.Budget: a caller-owned, lock-free
    pool of PageRank worker slots shared by concurrent calls. A run takes the slots it can without
    blocking and runs on a single worker when fewer than two are free; a nil Budget keeps the
    previous behaviour, and ranks are bit-identical across budgets (rmp #3086, fc0f6807).
  • graph.(*Mapper).AppendIDs, graph.MapperCursor and (MapperCursor).Done: append node
    ids in Walk order in bounded batches from a cursor, holding each shard's read lock only while
    its ids are copied (rmp #3081, 1a8e25ae).

Changed

  • cypher/exec.(*ColumnarFilter).Close is now declared on ColumnarFilter itself; before,
    the method was promoted from the embedded Filter. It returns the operator's scratch batch to
    the node-scan chunk pool, then closes the child. The method set is unchanged
    (rmp #3114, 3e2f9a2d).
  • graph/csr.(*CSR).BuildReverse returns the receiver itself when KnownSymmetric reports
    true, instead of a copy; both are immutable (rmp #3078, cfd64290).
  • search.DiameterCtx computes the exact diameter with BoundingDiameters (Takes and Kosters,
    CIKM 2011) instead of an iFUB level walk, running sweeps in batches of GOMAXPROCS above 1 024
    vertices. The result, the seed rule and the disconnected-graph and cancellation contracts are
    unchanged. Vertex-transitive shapes need one sweep per vertex: a 600-vertex cycle is 203 %
    slower; the O(n(n+m)) worst case is unchanged (rmp #3085, 6b399a3b).

Fixed

  • graph/lpg: an edge-property append never rewrites a cell a pinned reader can see. After a
    WithdrawTx rollback restored a pre-image, a later append to a block with more than two
    columns or a bool column could overwrite a cell a lock-free reader of the aborted entry was
    still reading: a data race and a changed value. Appends now write in place only at the column's
    own high-water mark (rmp #3093, ce609ce6).
  • cypher: a Go API edge is no longer reported with a Cypher sibling's relationship type.
    The stable edge handle on a row was read as a CSR slot position, so after deletes on a node pair
    holding both Cypher-created and Go API edges, a Go API edge could take the type of a sibling
    (rmp #3088, 4e9c48bb).

Performance

Figures are development-time measurements recorded by the commit that shipped each change,
against that commit's own control. They are not a...

Read more

v0.16.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 04:40

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.mod toolchain directive, was go1.27.0). The
    vulnerability database of 2026-10-08 lists 13 standard-library vulnerabilities in go1.27.1,
    9 reachable from GoGraph (net/http, crypto/tls, net/textproto, mime/multipart), from
    GO-2026-6599 to GO-2026-6617. With go1.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
    file wal.control carrying a random, per-store id, and numbered 16 MiB segments under wal.d/.
    Each frame has a 36-byte header (frame version 2) 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-file wal; the first writable clean open seals it with a LegacySeal control
    record, writes the control file and continues in segments. The sealed legacy log stays readable.
    A migrated store cannot be opened by v0.15.0 or 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 with wal.ErrLegacyNotSealed. Take a copy of the
    store directory before the first open with v0.16.0 if 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-back CREATE or the commit order no longer moves a node's id() 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 a ReserveIDs control 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, WALFormat and WALRedoPos fields. v0.15.0 reads 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 through AddEdge alone, 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 durable wal.prefix-truncated marker, 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).TruncatePrefix and store/wal.ErrPrefixTruncateUnsupported
    (segments are unlinked instead); store/recovery.Result.WALTailOffset (replaced by WALEnd,
    WALLastFramePos, WALTailSegment and WALTailSegmentOffset); 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, Revive and
      RestoreTombstones now return error.
    • store/snapshot.ApplyTombstonesToGraph now returns error.
    • graph.(*Mapper).LoadFrom takes a second argument, the per-shard high-water marks
      (*[graph.MapperShards]uint64).
    • cypher/ast.ExistsSubquery.Query and cypher/ast.CountSubquery.Query are now ast.Query
      (a possibly multi-branch query) instead of *ast.SingleQuery, and
      cypher/ir.TranslateSubquery takes an ast.Query.
    • cypher/exec.MergeActionEvalKey takes the action's ordinal as its first argument.
  • Widened, source-compatible: store/recovery.ReplayWAL takes a wal.FrameSource, which
    *wal.Reader satisfies.

Changed — BREAKING: behaviour

  • One token limit on every layer: 65535 bytes (rmp #2748). A node label, a
    relationship type or a property key longer than lpg.MaxTokenLen (65535) bytes is
    refused with an error wrapping lpg.ErrTokenTooLong, before any state change, by the
    in-memory engine (graph/lpg), the WAL-backed store (store/txn, whose
    ErrTokenTooLong is 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.CheckToken validates 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, SetEdgePropertyByHandleID and DelEdgePropertyByHandleID
      now return error; SetEdgeRelTypeAtSlotByID and AddEdgeRelTypeOverflowByID now
      return (bool, error).
    • graph/lpg.WriteView: RemoveNodeLabel, DelNodeProperty, SetEdgeLabel,
      RemoveEdgeLabel, DelEdgeProperty, SetEdgeLabelAt, SetEdgeLabelByHandle and
      DelEdgePropertyByHandle now return error.
    • graph/lpg.LabelRegistry.Intern and graph/lpg.PropertyKeyRegistry.Intern now return
      (id, error).
    • graph/lpg/schema.Schema.RegisterLabel now returns (lpg.LabelID, error).
    • store/snapshot.ApplyEdgeHandlesToGraph now returns error.
    • cypher/exec.GraphMutator: RemoveNodeLabel, DelNodeProperty, SetEdgeLabel,
      DelEdgeProperty, SetEdgeLabelAt, SetEdgeLabelByHandle and
      DelEdgePropertyByHandle now return error.
  • A direct write is a single-operation transaction (rmp #2947). A mutator of
    graph/lpg.Graph called outside any transaction — and a Graph.Writer view over the
    zero WriteTx — 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 new lpg.ErrDirectWriteConflict and 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 returned nil and was lost when that transaction rolled back, and a
    direct edge or node removal could be left half applied. Separately, Graph.ApplyVersioned and
    `Graph...
Read more

v0.15.0

Choose a tag to compare

@github-actions github-actions released this 17 Sep 14:19

GoGraph v0.15.0 — see CHANGELOG.md for the per-release entry.

[0.15.0] — 2026-09-17

33 commits — 21 perf, 8 docs, 1 fix, 1 build and 2 merges. Counted at 19138042,
the develop tip before release preparation; the release-preparation commits that carry this
entry are necessarily not in their own count. One sprint delivered this window — 362,
performance laboratory 20260915 — and all 33 of its tasks closed.
468 files changed,
238 586 insertions, 963 deletions (git diff --shortstat v0.14.2..19138042).

Sprint 361 is not in this window. Its twelve tasks closed at a7f06ec3, df0b1866 and
02c08e44, and all three are ancestors of v0.14.2 — verified with
git merge-base --is-ancestor. It shipped in v0.14.2 and is recorded in that entry.

238 586 insertions is not 238 586 lines of engine, and the difference is larger here than in
any previous release.
Bucketed by path from git diff --numstat v0.14.2..19138042:

bucket files insertions deletions
docs/benchmarks/ (raw measurement artefacts) 373 217 756 0
*_test.go + testdata/ 33 7 648 21
examples/ (exercise harnesses) 13 4 739 440
docs/ (prose) 10 3 605 38
engine *.go (non-test, shipped) 24 2 270 381
scripts/ (laboratory drivers) 9 2 150 0
build/CI (Makefile, .github/) 2 219 30
internal/ (not shipped API) 1 154 43
root Markdown + misc 3 45 10
total 468 238 586 963

91.3 % of the insertions are measurement data. The raw benchstat series, profiles and host
telemetry under docs/benchmarks/ are reproduction input for the three campaign records, not
code and not documentation; goreleaser excludes them from the release tarballs by design.

The exported API gained exactly two identifiers and lost none. Measured by parsing every
non-test Go file outside internal/, examples/, cmd/ and any gen/ or tck/ directory at
both v0.14.2 and 19138042, and diffing the rendered declarations: 4 820 exported
declarations before, 4 822 after, with two additions and zero removals, renames or
re-signings
. That file set is a superset of the API surface
docs/semver.md defines — it retains all of bench/, of which the policy
counts only bench/ldbc, bench/dimacs9 and bench/rmat — so it cannot miss a change inside
the surface. No bench/ file changed in this window, so the two-addition result is the same
under either reading. Under docs/semver.md a new exported identifier is the MINOR
trigger, so 0.15.0 is a MINOR by the rule rather than by decision — unlike v0.14.1,
where a MINOR surface shipped as a PATCH deliberately.

No change is breaking. cypher/exec.ExpandConfig.EmitStoredDir defaults to false, so
every existing caller's row width and chunk schema are unchanged; graph/index/label's
Intersect and Union keep their caller-owned-result contract, and a test deliberately
mutates a single-label Intersect result to assert it. No on-disk format changed — no file
under store/ was modified in this window.

go.mod and go.sum are byte-identical to v0.14.2 — same pinned toolchain go1.27.0,
same go 1.26 directive, same dependency set at the same versions
(git diff v0.14.2..19138042 -- go.mod go.sum is empty). Nothing in the supply chain moved.

The openCypher TCK gate is unchanged at 3 897/3 897. const tckExecutionBaseline = 3897
in cypher/tck/runner_test.go is untouched and no .feature file changed in this window,
so the scenario population is the one v0.14.2 was measured against. Every campaign re-ran it
and it held throughout.

Every performance claim below rests on an interleaved A/B against a byte-identical noise
control measured in the same session.
That discipline is not decoration: on this host a
same-versus-same arm has produced 22 of 36 flat-by-construction rows as statistically
significant, so an uncontrolled comparison on it means nothing. Where a figure is a share of a
profile rather than an absolute, it is labelled as such.

Seven premises were refuted on measurement and shipped no code, including two float
reassociations that were built in full, validated, measured and then discarded. They are
itemised under Notes rather than dropped, because a refuted premise is a result.

All 33 commits in this window are unsigned (git log --format=%G? returns N for every
one). docs/release.md asks for this to be re-measured per release rather
than assumed; this is that measurement, and it has not changed.

Added

  • graph/index/label.(*Index).BitmapShared(label uint32) *roaring64.Bitmap — returns the
    memoised, immutable label bitmap image without copying it. The caller must not mutate the
    result. It exists because Intersect and Union promise a caller-owned result and that
    promise is kept: the new method is the opt-in way to read the shared image on a scan path that
    does not mutate. *roaring64.Bitmap was already in this package's exported signatures
    (Intersect, Union), so no new third-party type enters the public API (#2863).
  • cypher/exec.ExpandConfig.EmitStoredDir bool — opt-in, defaulting to false. When set,
    Expand appends a fourth column to every emitted row carrying the edge's stored
    orientation relative to the emitted traversal orientation: false for a row the forward pass
    produced, true for one the reverse pass produced. It is meaningful only for DirBoth, the
    one direction under which the orientation varies row by row; a DirOut or DirIn hop's
    orientation is a property of the hop and is resolved at plan time. A caller that sets it
    owns registering the fourth schema column
    , or the row and schemaWidth disagree. The cell
    is an expr.BoolValue, read by type assertion, so a coordinate gone stale across a
    projection falls back to the old ladder instead of reading a node id as a direction (#2864).
  • Two counters, cypher.csr_pair_cache.bypasses_no_cache and
    cypher.csr_pair_cache.bypasses_own_writes.
    Together with the pre-existing hits and
    misses they form an exact partition of every cache consultation, which is what makes a
    measured hit rate readable at all. Both are documented in
    docs/metrics.md (#2866).
  • -contention flag on examples/internal/exprof, writing mutex, block and goroutine
    profiles, off by default. examples/ is explicitly outside the public API in
    docs/semver.md — the module neither imports nor depends on it — so this is
    recorded for completeness and is not API surface (#2856).
  • Three campaign records and three profiling reports under docs/, plus two prior-art
    studies read from Neo4j, Memgraph and PostgreSQL source at pinned tags:
    campaign-bolt-extreme-concurrency-2026-09-16.md, campaign-cypher-parsing-2026-09-16.md,
    campaign-whole-surface-2026-09-16.md, profile-bolt-concurrency-2026-09-16.md and its
    round-2 and round-3 successors, prior-art-cypher-parsing-2026-09-16.md and
    prior-art-mvcc-visibility-2026-09-16.md.

Changed

  • The Bolt server's per-rejection log is bounded to one line per second, and its argument is
    lazy
    (#2835). Refusing 500 connections now produces 1 log line where it produced 500;
    bolt.server.conn.rejected still counts exactly 500, so the operator keeps the signal
    and loses only the storm. RemoteAddr is resolved 0 times at LevelError and 1 time
    at LevelWarn across 100 refusals. The bound is on time, not a one-in-N sample: a
    count-based sample still grows linearly with the flood's rate, which is the property that had
    to go. rejectLogInterval is a fixed one-second package constant following the
    handshakeTimeout precedent, deliberately not an Options field, so the change does not
    widen the public API. If you parse or alert on this log line's volume, read this entry.
    Admission, the semaphore and its capacity are byte-identical.
  • The Bolt server writes one response per query instead of two (#2834). sendResponse no
    longer flushes; the session loop carries one obligation instead — the response buffer is
    drained before the goroutine can block waiting for the client, and before teardown —
    discharged in exactly two named places. Teardown flushes under a fixed deadline, so a peer
    that has stopped reading can no longer pin a handler goroutine and its semaphore slot. RECORD
    batching is unchanged and its three regression tests are untouched. Protocol bytes are
    unchanged; what changed is how many write(2) calls deliver them.
  • propMapShards moves from 64 to 256 (#2839), and the cost is recorded rather than
    hidden: an empty graph grows 91 KiB and New takes 57 % longer, because New eagerly
    allocates one map per stripe in three of the nine shard arrays. This is a level shift, not
    a change of scaling behaviour
    — the collapse from 64 to 1 024 connections moves only 0.660
    to 0.698 of peak. 4 096 shards was rejected on measurement for penalising the
    single-threaded scan (+43.8 %) and write (+41.7 %); a full-scan read at 256 stays
    indistinguishable from 64, so the read path the 16→64 widening bought is not surrendered.
  • make ci runs the module suite exactly once and fails cheap first (cd5cee02). The
    coverage gate leaves the automatic gate by user decision, as benchmarks did in v0.14.2;
    make cover-gate, scripts/cover_gate.sh and their thresholds are untouched and are run
    deliberately. CI_STAGES now carries membership and order — shell-guard tidy fmt vet build ci-kg-verify vulncheck lint test-uninstrumented test-timing test-short — driven by the
    new scripts/ci_stages.sh, which stops at the first failure, times each stage and stamps the
    ones that pass. vulncheck and `ci...
Read more

v0.14.2

Choose a tag to compare

@github-actions github-actions released this 15 Sep 21:04

GoGraph v0.14.2 — see CHANGELOG.md for the per-release entry.

[0.14.2] — 2026-09-15

9 commits — 3 fixes, 4 documentation changes and 2 merges. Counted at 92c47fda, the
branch tip before release preparation; the release-preparation commits that carry this entry
are necessarily not in their own count. One sprint delivered this window — 361, bug fixing
campaign 26.09.15
— and all 12 of its tasks closed.
25 files changed, 3 235 insertions,
103 deletions (git diff --shortstat v0.14.1..92c47fda).

The 12 closed tasks and the 3 fix commits that carry them match in both directions — twelve
ids in the sprint, twelve distinct ids across the commit bodies, empty both ways, with each fix
commit's Refs footer naming exactly four. This is not a 1:1 task-to-commit mapping and is
not claimed as one: the sprint was worked as three batches of four tasks sharing a scope, which
is what the project's batching rule asks for. The remaining six commits are four documentation
commits, the sprint merge and the v0.14.1 back-merge.

What the insertion count is, because 3 235 lines is not 3 235 lines of engine. Bucketed by
path from git diff --numstat v0.14.1..92c47fda:

bucket files insertions deletions
*_test.go (regression tests) 13 2 254 10
engine *.go (non-test, shipped) 5 354 30
internal/sim/ (the simulator's oracle, non-test) 4 318 15
root Markdown (CLAUDE.md, README.md) 2 255 46
docs/ (prose) 1 54 2
total 25 3 235 103

The shipped engine change is 354 insertions across five files, and one of those five is
godoc.
bolt/server/serve.go (+23/−4) changes comments only and has no compiled effect, so
four files carry the whole of this release's behaviour: cypher/api.go (+181/−21),
bolt/server/errors.go (+66/−2), cypher/exec/set.go (+53/−3) and cypher/exec/set_all.go
(+31/−0). Against that stand 2 254 lines of regression test across 13 files — the shape a
bug-fixing sprint should have, and the sprint's own rule: every fix closes with a test that
fails on the old behaviour and passes on the corrected one.

One defect runs through this entire release: the module said one thing and did another.
Every item below is an instance of it. A SET reported success and discarded the value
(#2816). An undirected SET reported two relationship writes and made one (#2817). A
DROP INDEX … IF EXISTS reported a removal that never happened (#2818). A statement that
failed and rolled back still reported the counters for the work it had just undone (#2823).
Three faults the client had committed reached it as "An internal error occurred" (#2819).
Three godocs asserted intent the code did not implement (#2811, #2824), and one counter
counted more than its name admits (#2791). This is a release about the gap between what
GoGraph reports and what GoGraph does, and it closes that gap in every place the sprint
found it.

Two of the twelve tasks refuted their own premise and shipped no fix (#2815, #2820). Both
are reported in full under Notes rather than quietly dropped, because a report that turns
out not to reproduce at HEAD is a result, and the regression coverage written to establish
it ships with the release.

go.mod and go.sum are byte-identical to v0.14.1 — same pinned toolchain, same
dependency set at the same versions (git diff v0.14.1..92c47fda -- go.mod go.sum is empty).
There is nothing to report under a dependency heading and nothing is invented to fill one.

The exported API is unchanged, and that is what makes this a PATCH. Five functions were
added and all five are unexported — relStorageDirection, isEntityPropertyValue,
exprValueIsEntity, errEntityPropertyValue and dropIndexCounter. No exported identifier
was added, removed or re-signed, and no exported struct gained a field.
Under
docs/semver.md a new exported identifier is the MINOR trigger and there is
not one, so 0.14.2 is a PATCH by the rule rather than by decision — which is the opposite of
v0.14.1, where a MINOR surface was shipped as a PATCH deliberately. There is therefore no
Added section in this entry
, and the heading is omitted because there is genuinely nothing
to put under it, not because the surface went unexamined.

No change is marked breaking — no commit in the window carries a BREAKING CHANGE footer
or a ! subject, verified over the full commit bodies rather than the subjects alone. Three
behaviours a caller can observe do change, and every one of them replaces a wrong answer with
a right one; they are itemised under Fixed and revisited in
release-notes/v0.14.2.md.

The openCypher TCK gate is unchanged at 3 897/3 897. const tckExecutionBaseline = 3897
in cypher/tck/runner_test.go is untouched and no .feature file changed in this window,
so the scenario population is the one v0.14.1 was measured against.

All 9 commits in this window are unsigned (git log --pretty=%G? returns N for every
one). docs/release.md asks for this to be re-measured per release rather
than assumed; this is that measurement, and it has not changed.

Fixed

Cypher — the effect a statement reports now equals the effect it applied

  • A SET no longer discards a node-, relationship- or path-valued property while reporting
    success
    (#2816). isStorableProperty rejected only maps, so five write paths each read its
    ok=false as "no value was produced" and moved on. The defect was wider than reported: on
    the REPLACE forms the dropped entry still cleared the existing keys, so SET a = {other: b}
    answered ok with propertiesRemoved=1 and left the node with no properties at all —
    silent destruction of data the caller never asked to remove. All five sites now raise
    InvalidPropertyType. Gated by cypher/set_entity_property_value_test.go.
  • An undirected SET over a reciprocal relationship pair now writes both relationships
    (#2817). resolveRelBinding keyed the edge-property mutation on the row's traversal order,
    and the reverse hop of an undirected pattern is the swap of the edge's storage order — so
    both rows addressed the same stored edge, one edge was stamped twice and the other never
    touched. id(r) returns two distinct relationships, so under openCypher 9 relationship
    isomorphism the statement owes two rows and two writes: the counter was right and the write
    was missing.
    relStorageDirection normalises the target to storage order. Gated by
    cypher/set_undirected_reciprocal_test.go and
    cypher/exec/set_rel_storage_direction_test.go.
  • DROP INDEX … IF EXISTS no longer counts a removal that never happened (#2818). Both
    branches of runDropIndex bumped IndexesRemoved unconditionally, so absorbing a missing
    index reported indexesRemoved: 1 over an index listing identical before and after.
    dropIndexCounter now reads for existence before the operator runs and reports the effect
    actually applied. WAL behaviour is unchanged — nothing was ever written on the absorbed
    path; only the report was wrong. This fix has a measured cost, stated under Performance.
  • A statement that failed and rolled back no longer reports the counters for the work it
    undid
    (#2823). rollbackUnderBarrier correctly reversed every eager mutation and then left
    r.counters pointing at them, so a caller that inspected the result of a failed statement was
    told about writes that no longer existed. It is not MERGE-specific as reported — a plain
    multi-assignment SET and a CREATE … WITH … SET reproduce it identically. The evidence for
    the corrected behaviour is unanimous across three independent sources. The TCK corpus never
    asks for counters from a statement that raised.
    Of the 1 615 scenario blocks in
    cypher/tck/features/, 192 carry a "should be raised" step and 1 423 carry a side-effect
    assertion, and the intersection is empty.
    The 1 423 counts both forms the TCK uses, because
    both assert about side effects: 244 state them as an explicit the side effects should be: table and 1 179 use no side effects, which asserts just as positively that the
    statement changed nothing (244 + 1 179 = 1 423 exactly, so no block carries both forms). It is
    still empty on the executed population, where those 1 615 blocks expand to the 3 897 scenarios
    the baseline counts: 695 raising, 3 202 asserting side effects, 0 both. A Bolt FAILURE
    message carries no stats field, and the reference driver returns no summary at all when the
    query failed. Gated by
    cypher/failed_statement_counters_test.go.

Bolt — three client faults the server was masking

  • Three errors a client can act on now reach it as client errors instead of
    "An internal error occurred"
    (#2819), and they were masked for three different reasons.
    txn.ErrFieldTooLong had no FailureCode case at all; the unsupported-DDL refusal arrives
    untyped, as a wrapped fmt.Errorf; and validatePreExisting's NOT NULL arm wrapped only the
    bare sentinel where errors.As needed the type. They are now
    Neo.ClientError.Statement.ArgumentError, Neo.ClientError.Statement.SyntaxError and
    Neo.ClientError.Schema.ConstraintValidationFailed respectively. Each code follows
    precedent already in the module rather than being invented for the occasion
    , and the
    reasoning is recorded beside the branch that uses it:
    Neo.ClientError.General.LimitExceeded was considered for the over-long field and
    rejected, because rmp #2561 established that it does not appear in Neo4j's status codes
    at all. Every forwarded
    message was checked to carry only static text, the client's own tokens and a format
    constant
    — no server path, no internal identifier — and that check is now asserted
    permanently
    ...
Read more

v0.14.1

Choose a tag to compare

@github-actions github-actions released this 09 Sep 03:54

GoGraph v0.14.1 — see CHANGELOG.md for the per-release entry.

[0.14.1] — 2026-09-09

50 commits — 26 fixes, 7 documentation changes, 6 test changes, 3 features, 3
performance changes, 3 build changes, 1 chore and 1 merge. Counted at efd32fb9, the last
commit before release preparation; the release-preparation commits that carry this entry
are necessarily not in their own count. One sprint delivered this window — 357, clear
every open bug and chore, and ship GoGraph v0.14.1
— and 46 of its 47 tasks closed.
The
forty-seventh, #2812 (an indexed-MERGE index probe), was carried to sprint 358 and is
not in this release. 184 files changed, 33 244 insertions, 1 679 deletions
(git diff --shortstat v0.14.0..efd32fb9).

Every one of the 46 closed tasks has exactly one commit in this window, and every commit
carrying a Refs line names one of them.
That is a 1:1 mapping, established by comparing
the two sets rather than asserted: 46 task ids in the sprint, 46 distinct task ids across
the commit bodies, empty in both directions.

What the insertion count is, because 33 244 lines is not 33 244 lines of engine.
Bucketed by path from git diff --numstat v0.14.0..efd32fb9:

bucket files insertions deletions
*_test.go 93 20 921 374
*.go (non-test) 71 9 343 745
docs/ (prose) 9 1 980 341
root Markdown (README.md, knowledge-model.md) 2 635 193
.rmp, cmd/kgverify/baseline.json 2 112 1
build, CI and release tooling 5 106 2
release-notes/v0.14.0.md (the erratum) 1 95 9
examples/ (one README) 1 52 14
total 184 33 244 1 679

The engine change is 9 343 lines across 71 files, and the largest single bucket is tests —
which is the shape a bug-clearing sprint should have: every fix in this window carries a
regression test that fails on the old behaviour and passes on the new, by a sprint rule
carried over from sprint 350.

Three things define this release.

A MATCH on an indexed property could return a wrong answer, and did in the shipped
v0.14.0.
The equality index seek was blind to writes the statement itself had made, so
it both lost rows the transaction had written and returned rows that no longer
matched
— with no explicit transaction needed, on a single statement. #2814 declines the
index access path for the coordinates the transaction has dirtied, keyed per
(label, property), which turns a wrong answer into a slower correct one. This is the most
important item in the release and the reason it exists.

Four index-build paths could put a row in an index that the graph does not contain, or
leave one out.
The backfill read uncommitted mutations, so a seek could return a
fabricated row (#2778); CREATE CONSTRAINT's backfill had the same defect (#2792);
FinishBuild's replay resolved against the live property bag, so a concurrent write could
fabricate an entry (#2793); and rewindConstraintDrop read live state, where the obvious
fix traded a fabricated entry for a lost one (#2799). Because a store written by an
affected build can hold fabricated payloads on disk, #2797 stamps an index-builder
epoch into the snapshot manifest and recovery now refuses to hydrate a payload written
before the fix, rebuilding the index once instead.

The durability path stopped calling three different failures clean. The checkpoint
self-sufficiency gate matched filenames and never verified the snapshot was readable, so a
WAL prefix could be discarded against a snapshot nothing could load (#2749); the readback
that fixed it covered the recovery reader but not its applier (#2780); ReplayWAL
reported corruption inside an already-committed frame as benign, contradicting its own
documented contract (#2794); and a producer op cap above the replay cap made committed data
unreplayable rather than merely rejected (#2530).

go.mod and go.sum are byte-identical to v0.14.0 — same pinned toolchain, same
dependency set at the same versions (git diff v0.14.0..efd32fb9 -- go.mod go.sum is
empty). There is nothing to report under a dependency heading and nothing is invented to
fill one.

No change is marked breaking — no commit in the window carries a BREAKING CHANGE
footer or a ! subject, verified over the full commit bodies rather than the subjects
alone. The exported surface did grow: 31 new exported declarations and 6 new exported
struct fields, with none removed. Under docs/semver.md a new exported
identifier is the MINOR trigger, so the delta is itemised in full under
Added below rather than summarised, and the two changes a caller can actually
feel — four exported default constants that changed value, and one exported function whose
declared return type changed — are set out under
Changed below, with the evidence for each.

This release is numbered 0.14.1 — a PATCH — by explicit decision, not by oversight.
Under docs/semver.md the 31 new exported identifiers are a MINOR trigger,
and PATCH is otherwise reserved for changes that preserve every previously-documented API
contract; four exported constants changing value does not preserve them. The number was
nonetheless kept at PATCH deliberately. So do not read 0.14.1 as a drop-in upgrade:
treat the Changed section below as the checklist a MINOR bump would have signalled. The
two items that will reach a caller silently are DefaultConnTimeout, DefaultTxTimeout,
DefaultMaxTxIdleTime and DefaultStatementTimeout, every one of which is now 0 —
disabled — where three of them were 30 s, 30 s and 5 s; and NewServer, which now
returns an error for a negative timeout it previously coerced to a default in silence.

The openCypher TCK gate is unchanged at 3 897/3 897. const tckExecutionBaseline = 3897
in cypher/tck/runner_test.go is untouched and no .feature file changed in this window
(git diff --name-only v0.14.0..efd32fb9 -- 'cypher/tck/features/**' is empty), so the
scenario population is the one v0.14.0 was measured against.

Read release-notes/v0.14.1.md before upgrading. There is
no data migration, but a store written by v0.14.0 or earlier rebuilds its secondary
indexes once on first open, and the Bolt server's four timeout defaults are now disabled.

Fixed

Cypher — the index access path returned wrong answers

  • A MATCH on an indexed property no longer loses rows the statement wrote, nor returns
    rows that no longer match
    (#2814). The equality hash-index seek and the range seek both
    read the committed index while ignoring the transaction's own pending mutations. Two
    wrong answers followed from one cause: a row created or updated in the same statement was
    missing from the result, and a row whose indexed property had been changed away from
    the sought value was still returned. It needed no explicit transaction — a single
    autocommit statement was enough. The fix tracks the (label, property) coordinates a
    transaction has dirtied and declines the index access path for exactly those, falling back
    to the label scan; correct and slower beats fast and wrong. EngineOptions.DisableIndexSeek
    is the kill switch. Gated by cypher/index_seek_uncommitted_write_test.go, mutation-tested
    from both sides: disabling the guard turns all four behavioural cases red, and a blanket
    decline kills the precision cases, so "never seek on a write path" could not have passed
    for it.
  • An index build no longer reads uncommitted mutations, so a seek cannot return a
    fabricated row
    (#2778). CREATE INDEX backfilled from the live property bag, which
    includes writes no reader should see; a row that was never committed could therefore be
    indexed, and a later seek would return it. The build now runs against a snapshot.
  • CREATE CONSTRAINT's backfill had the same defect and is fixed the same way (#2792),
    and its uniqueness seed is now taken from the same read its index is built from, so the
    two cannot disagree.
  • FinishBuild's replay answers with the graph it recorded, not the graph it finds
    (#2793). Concurrent writes were reconciled against live state at the end of the build, so
    a write that landed mid-build could fabricate an entry. The replay now resolves against
    the recorded change log, which is bounded at MaxBuildLogChanges and reports overflow as
    ErrIndexBuildOverflow rather than silently truncating.
  • rewindConstraintDrop rebuilds from committed state (#2799). Reading live state
    fabricated an entry; the first fix considered traded that for a lost one, and both
    failure directions are now covered by tests.
  • CREATE CONSTRAINT … IS NOT NULL no longer succeeds over committed data that violates
    it
    (#2798). Validation ran against live state only, so a violating row that was already
    durable was not seen. It is now validated against committed state as well.
  • An explicit transaction now takes the schema gate, so its commit-time index fan-out
    cannot escape a DDL barrier
    (#2738), and concurrent writes are reconciled into an index
    built without excluding them.

Cypher — subqueries, projections and reported counts

  • COUNT { … } block form counts the rows its body returns (#2675). TranslateSubquery
    dropped the body's trailing RETURN, so the count was taken over the wrong row set. The
    same defect in existsSubPlan is fixed separately (#2779), where the naive fix was
    measured and refuted before the working one was found; the SemiApply inner schema is
    now isolated and the body's RETURN translated.
  • Pattern predicates and EXISTS/COUNT subqueries now run inside a writing statement
    (#2660). The write path never wired the subquery and pattern evaluators, so a writing
    statement carrying either refused to run. Five COUNT block-form ...
Read more

v0.14.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 15:18

[0.14.0] — 2026-09-06

13 commits — 5 features, 4 fixes, 1 performance change, 1 documentation change, and
2 merges. Counted at 94602f46, the merge that closed sprint 355 into develop and the
commit release/0.14.0 was cut from; the release-preparation commits that carry this entry
are necessarily not in their own count. One sprint delivered this window — 355,
GoGraph execution reporting — and all 11 of its tasks closed.
158 files changed,
22 858 insertions, 661 deletions (git diff --shortstat v0.13.0..94602f46).

What that insertion count is, because 22 858 lines is not 22 858 lines of engine:
9 841 insertions across 38 _test.go files, 5 797 across 79 raw measurement files under
docs/benchmarks/*-raw/, 4 470 across 33 non-test .go files, 2 708 across 7 documentation
files, and 42 in one example README. Bucketed by path from
git diff --numstat v0.13.0..94602f46.

Three things define the release.

Execution reporting stopped claiming what it had not measured. The DbHits column could
not distinguish "counted, and it is zero" from "never counted at all" — both printed 0. It
is now tri-state: a counted figure, a counted zero, and ? for a figure nobody counted, with
an incomplete total rendering x + ? rather than silently summing across the gaps. Four
operator families that read storage and reported nothing now count it, and each is proved by
an observed arm in which the db-hits figure differs from the operator's own row count, so
the figure cannot have been derived from it. A new Removed column reports how many candidate
rows a predicate threw away.

The planner consumes its own statistics for the first time. The property statistics have
been maintained since #2097 and, until this release, were read by nothing but the EXPLAIN
renderer. They now decide the drive order of the disjoint-component join reorder, under the
pre-existing trustworthiness veto and evaluated over a certified error interval rather than at
the point estimate. Est.Rows renders beside the measured Rows with its provenance
marked, so an estimate and the measurement that tested it can be read from one table over one
plan; and how wrong the estimates turn out to be is itself observable, as a q-error metric.

The sprint's own gate found two defects, and both were fixed rather than deferred. A
declined MVCC label count was read as an empty label, which demoted the estimate, made the
planner benefit non-deterministic, and let EXPLAIN render a plan the engine did not run
(#2771). And a labelled count cloned a roaring bitmap and returned it uncorrected, purely
to have GetCardinality() called on it (#2773).

go.mod and go.sum are byte-identical to v0.13.0. Same pinned toolchain, same
dependency set at the same versions (git diff v0.13.0..94602f46 -- go.mod go.sum is empty).
There is nothing to report under a dependency heading and nothing is invented to fill one.

No change is marked breaking — no commit in the window carries a BREAKING CHANGE footer
or a ! subject — but the rendered plan output changed shape, and any consumer that parses
plan text or reads the Bolt plan map may need updating. The full list is under
Behaviour a caller can observe — the compatibility surface.

The openCypher TCK gate is unchanged at 3 897/3 897. const tckExecutionBaseline = 3897
in cypher/tck/runner_test.go is untouched and no .feature file changed in this window
(git diff --name-only v0.13.0..94602f46 -- 'cypher/tck/features/**' is empty over 220
feature files), so the scenario population is the one v0.13.0 was measured against.

Read release-notes/v0.14.0.md before upgrading. There are no
data-migration steps, but the observable plan output changed and the release's open gaps are
listed there.

Added

Cypher — the estimate reaches the physical plan

  • cypher/exec.PlanEstimate, exec.EstimateSource with EstimateAbsent, EstimateExact,
    EstimateStats and EstimateHeuristic, and the methods EstimateSource.Known,
    EstimateSource.Exact and EstimateSource.String
    (#2765) — the planner's cardinality
    estimate and its provenance, as carried on a physical plan node.
  • cypher/exec.PlanEstimates, exec.PlanTreeWithEstimates (#2765) — the operator-to-
    estimate mapping and the plan-tree builder that applies it. Attribution is a single rule at
    buildOperator, the one point holding both a logical node and the operator it lowered to:
    the deepest logical node whose lowering returns an operator owns the estimate, and it is
    never overwritten.
  • cypher/exec.EstRowsCell, exec.EstRowsAnnotation and const EstRowsUnknown = "-"
    (#2765) — the Est.Rows cell renderer, the tree annotation, and the glyph for an operator
    with no derivable estimate. Four plan shapes have no logical node at all — the range-seek
    leaf, the min-label re-anchored scan, the parallel leaves and the columnar fusion chains —
    and render - rather than borrowing a neighbour's number.
  • cypher/exec.PlanNode.Est (#2765), the field the renderers read.

Cypher — db-hits gains an honest unknown

  • cypher/exec.PlanNode.DbHitsKnown, exec.DbHitsCell, exec.DbHitsTotalCell and
    const DbHitsUnknown = "?"
    (#2760) — the explicit unknown state and its renderers. A
    separate boolean was preferred to Neo4j's OperatorProfile.NO_DATA = -1 sentinel so that no
    magic number can leak into arithmetic.
  • cypher/explain.ProfileReport.TotalDbHitsUncertain (#2760), which is what makes the
    Total row render x + ? instead of a plain number when any contributing cell is unknown.
  • cypher/exec.PlanRows (#2760), which reports a plan's total rows together with whether
    that total is complete.

Cypher — rows removed by a filter

  • cypher/exec.PlanNode.RowsRemovedByFilter, PlanNode.RowsRemovedByFilterKnown and
    exec.RowsRemovedCell
    (#2764) — the rejected-row figure and its renderer. Filter,
    ColumnarFilter and Expand report it, through a rowsRemovedCounter marker with a
    compile-time census.

Cypher — the estimate error is observable

  • cypher.Engine.StatsMisestimatedPairs (#2767) — how many (label, property) pairs a
    PROFILEd plan has observed to be misestimated past the margin. It returns a count, not
    the pairs themselves. It counts only what a PROFILE has actually looked at, so a zero means
    "nothing observed to be wrong", never "everything is right"; and a successful
    Engine.RefreshStatistics clears it, so a non-zero reading after a refresh is the more
    interesting fact that the fresh statistics are still predicting badly. Safe for concurrent
    use.

graph/lpg

  • graph/lpg.Graph.LabelCountAsOf (#2773) — a labelled cardinality as of a snapshot that
    answers directly, never declines, and allocates nothing when nothing needs correcting.

Observability

  • Seven new cypher.stats.* metric series — cypher.stats.qerror and
    cypher.stats.qerror.high (#2767), cypher.stats.label_count.declined (#2771), and the four
    fallback-reason partitions cypher.stats.lookup.fallback.empty_label, .no_count,
    .no_statistic and .stale (#2771). cypher.stats.qerror observes
    max(est, act) / min(est, act) per operator of a PROFILEd plan, and
    cypher.stats.qerror.high counts the samples past the 3× margin. All eleven
    cypher.stats.* series are documented in docs/metrics.md; a set
    comparison of the series names in cypher/stats_metrics.go against that file is empty in
    both directions.

Changed

Behaviour a caller can observe — the compatibility surface

These are the release's compatibility surface. Nothing here is marked breaking, but each is
observable, and the first four change bytes a consumer may be parsing.

  • Engine.Explain gained an (est. rows=N exact) annotation on the physical plan tree
    (#2765). Where an operator has no derivable estimate, no annotation is added.
  • Engine.Profile, Engine.ProfileTable and Result.Profile() gained Est.Rows and
    Removed columns
    (#2764, #2765), and an uncounted db-hits cell now renders ? rather
    than 0
    (#2760), with an incomplete total rendering x + ?. Each column is dropped
    entirely when no operator in the plan carries that figure, so a plan with nothing to say in
    a column does not grow one.
  • exec.PlanNode gained exported fields — Est, DbHitsKnown, RowsRemovedByFilter
    and RowsRemovedByFilterKnown (#2760, #2764, #2765). explain.OperatorStats gained the
    same four, and explain.ProfileReport gained TotalDbHitsUncertain. Code that
    constructs any of these with an unkeyed composite literal will no longer compile; keyed
    literals are unaffected.
  • Bolt plan and profile metadata gained EstimatedRows, EstimatedRowsSource and
    RowsRemovedByFilter, and now OMITS dbHits for an uncounted operator
    rather than
    sending 0 (#2760, #2764, #2765). EstimatedRows is where Neo4j puts it, a plan argument;
    EstimatedRowsSource has no Neo4j counterpart, because Neo4j publishes the estimate
    unqualified. RowsRemovedByFilter is written for an EXPLAIN-shaped node too, unlike
    dbHits.
  • Anything parsing plan text or the Bolt plan map may need updating. A reader that assumed
    a db-hits cell is always an integer must now handle ? and x + ?; a Bolt client that
    assumed dbHits is always present must now handle its absence, which is the wire-level half
    of #2760.
  • Expand's db-hits figure changed definition, not merely scale (#2761). It reported the
    edges it emitted and now reports the adjacency slots it walked. On a source with 99 :LIKES
    and 1 :KNOWS out-edge, -[:KNOWS]-> previously reported dbhits=1 for the same 100-slot
    walk that --> reported as 100; both now report `...
Read more

v0.13.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 06:28

A performance and correctness-hardening release: sprints 352 and 353, the module's two deep-profiling and optimisation cycles. 100 commits, 86 closed tasks, 27 of them perf — against 4 in the whole of v0.12.0.

go.mod and go.sum are byte-identical to v0.12.0. Nothing in the supply chain moved.

Seven breaking changes — every one turns a silent failure loud

Pre-1.0 the minor digit absorbs them (docs/semver.md).

  • store/snapshot (#2743) and store (#2742) now refuse a field they would previously have truncated or written unreadably. The snapshot case was discarding the WAL behind a snapshot no reader could parse. New snapshot.ErrFieldTooLong, txn.ErrFieldTooLong.
  • graph/lpg (#2724, #2725, #2726) — WriteView.RemoveNode and RemoveEdge now return bool. A refused removal used to be journalled for undo anyway, so rollback resurrected the node or the arc. A rolled-back DELETE also no longer hides a committed node from older readers.
  • store/txn (#2727) — a panic in the commit window no longer wedges every later committer. No exported symbol changed; the ! marks an unexported signature.
  • cypher (#2721) — EXPLAIN and PROFILE are accepted as statement prefixes, so a Bolt driver can ask for a plan at all.

Two exported signatures broke without a ! marker and are documented as unmarked: cypher/exec.NewTop gained a parameter (#2509), and lpg.WriteView.RemoveAllEdgesFrom returns bool (#2694).

Measured against v0.12.0 — both trees, first-hand

Not carried forward: a worktree at the v0.12.0 tag and this candidate, each compiled once, run interleaved with the leading arm alternating, -race off, -count=6, load-gated. Identical go.mod between the arms means no toolchain or dependency change is mixed into any comparison — the first release since v0.11.0 for which that holds.

The noise floor was measured before anything was attributed: both arms on the same tree, 14 comparisons, 0 significant, largest drift 0.87 % sec/op, 0.00 % allocations.

Block geomean Largest single move
Cypher count pushdown −87.65 % Count_LabelStarBig_Serial −99.97 % (≈3 ms → 880 ns)
Contended metric emission, 1→1024 −92.79 % IncCounterParallel −96.47 % at 1024
Read transaction, 1→1024 −73.04 % ReadTx_LockFree −91.40 % at 1024
cypher/exec read-path operators −18.93 % Scan_PerNode −69.97 %; Sort_10k −43.59 %
Columnar query shapes −4.28 % five hop shapes −7.7 % to −9.4 %
search headline set (control) +0.83 % within noise, as a control must be

Contended metric emission changes sign, not degree: v0.12.0 anti-scaled (cost rising 5.8× from 1 to 1024 goroutines), v0.13.0 scales (falling 5.3×). Allocations collapse on two blocks — ReadTx_LockFree 249 → 37, Count_LabelStarBig_Serial 199,796 → 19 — and are constant across the whole ladder where v0.12.0's drifted with concurrency.

What got slower, and what this release cannot prove

Four regressions, none minimised. graph/index/btree Index_LookupHot +26.93 % (its own commit reports 6.66 %; no cause is attributed, because none was measured); Drain_Throughput +12.87 %; IncCounterParallel +8.32 % at one goroutine; CountAllNodes +3.09 %.

Sprint 353's headline index contention claims — 34.6× and 1.91× — cannot be verified at release level. No benchmark common to both trees exercises those indexes concurrently, because the instrument that measures them (bench/contention) is new here. They rest on each commit's own local A/B and are reported as such. Storage footprint, latency percentiles, and every non-darwin/arm64 host also remain unmeasured. docs/benchmarks/v0.13.0.md has the full list.

Gate

Read from inside the run log, not from a wrapper: MAKE_EXIT=0, 134 packages ok, 0 lint issues, govulncheck PASS over 136 packages in 11 modules, coverage 88.5 % aggregate with every package above its 75 % floor, and the openCypher TCK at 3897/3897 scenarios, 16006/16006 steps.

Two release blockers were found and fixed on the way, both in the instruments rather than the module: the read-path allocation gate was measuring a process-global counter that sibling goroutines contaminated (#2753), and cover-gate had never classified bench/entryheap as the instrument it declares itself to be.

Errata

The CHANGELOG entry for #2757 names a distribution route that does not exist. It says the false branch-protection claim "reached consumers" because .goreleaser.yaml bundles docs/**/* into every release tarball. Verified by extracting this release's own artefact: docs/release.md is not in the tarball, and neither is CONTRIBUTING.md. That sentence was written from reading the config instead of opening the artefact.

The substance is unaffected — the branch and tag protections genuinely do not exist, and the correction to both files stands. The claim did reach consumers, via GitHub and via go get, but not by the route named. Tracked as #2759.

That check also exposed a real packaging defect, tracked as #2758: docs/**/* matches only paths with a directory component, so 0 of 112 top-level docs/*.md files ship while 238 benchmark logs do. A consumer opening the tarball gets the evidence and none of the documentation. It will be fixed in the next release; v0.13.0 is already cached by the Go module proxy and cannot be recut.

What was delivered — from the project roadmap

3 sprint(s) closed in this release window (352, 353, 354), containing 91 tracked tasks. Every task in a closed sprint is COMPLETED: the roadmap tool refuses to close a sprint that still holds active work.

  • Sprint 352 — GoGraph module deep profiling and optimization
  • Sprint 353 — GoGraph Optimization Laboratory
  • Sprint 354 — release GoGraph v0.13.0 to production

Source: the project roadmap (rmp), not the commit log. Sprints are attributed to a release when their close date falls after the previous tag and on or before this one. Task counts are the sprints' own membership counts, read back from the tool.


Full detail: CHANGELOG.md · release notes · benchmark record

go get github.com/FlavioCFOliveira/GoGraph@v0.13.0

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 27 Aug 15:31

[0.12.0] — 2026-08-27

192 commits — 87 fixes, 51 features, 25 documentation, 18 test, 4 performance,
1 style, 1 chore, 1 build change, and 4 merges. Six sprints delivered work in this
window — 345–350 — and 176 tasks closed.

Three things define the release, and none of them is a new capability in the engine.

A deterministic-simulation-testing campaign drove every published surface.
Sprints 345–349 (110 tasks) took the DST harness in internal/sim from a
graph-and-storage exerciser to one that drives MVCC under true concurrency, the full
Cypher language surface, the storage/durability/MVCC substrate, the Bolt wire, and the
graph API, search and bulk-load surfaces. 50 of the release's 51 feat commits are
feat(sim)
, and internal/sim accounts for 127 267 of the 168 208 changed lines
(75.7 %)
. This is test infrastructure, not module functionality — the module's own
production code accounts for 8 564 lines across 93 files (5.1 %). What the campaign
delivers to a user is the defects it found: 26 module defects across those five
sprints, and 50 across the release once sprint 350's 24 are added.
Five of the
severity-9 ones were silent — wrong or lost data with no error anywhere.

And the crash model was repaired before it was trusted. The most consequential
result of the campaign is not a scenario but a control: until #2535, SimDisk
retained unsynced file data across a simulated host crash, so the battery could not
fail an engine that stopped calling fsync at all.
With a durable-length watermark
in place, deleting the WAL commit fsync now fails loudly —
checkpoint-crash-storm reports 192 violations at seed 610621185, losing exactly
half of 384 acknowledged commits, and 33 top-level tests fail
, where before the
change it failed nothing. Every durability claim this release makes about the
simulator rests on that.

The bug backlog was emptied. Sprint 350 (66 tasks) resolved every known bug,
re-validating each premise at HEAD before working it. Of the 66: 26 landed a fix in
product code
(24 in the module, 2 in examples/26_social_scale_bench), 7 premises
did not hold at HEAD
and are recorded below as refuted or obsolete rather than
reported as fixes, and 33 changed only tests, the simulator, the gates, benchmarks or
documentation
. No BUG-type task remains in the backlog.

Nothing breaks. No commit carries a ! marker or a BREAKING CHANGE footer, and
an audit of git diff v0.11.0..HEAD across the whole tree found no exported
identifier removed, no exported signature changed, and no exported struct field
dropped
. 51 exported identifiers were added across 15 packages, which is what
makes this a pre-1.0 MINOR release under docs/semver.md.
Every direct dependency, and every tool and CI pin, was raised to its latest release in
this window, so go.sum moved; the other go.mod change is the pinned toolchain
directive, go1.26.5 → go1.27.0, with the go directive left at go 1.26 so the
minimum Go a consumer needs is unchanged (see Changed).

The openCypher TCK gate is unchanged at 3897/3897 — 100 % execution-level
compliance is preserved, not extended. No .feature file changed in this window, so
the scenario population is the same one v0.11.0 was measured against.

Read release-notes/v0.12.0.md before upgrading. There
are no migration steps, but a dozen observable behaviours changed and are listed
there.

Added

Deterministic simulation testing — the campaign that found the defects

  • MVCC under true concurrency (sprint 345). A deterministic multi-session mode
    (RunMVCCSessions) over the durable store, per-transaction MVCC workspaces over the
    graph oracle, MVCC isolation checkers for snapshot stability, read-your-own-writes
    and atomic visibility (rmp #2436), a lost-update and write-conflict adjudication
    scenario (#2437), crash and recovery with open MVCC transactions (#2438),
    concurrent-mode transactional roles with typed conflict accounting (#2439),
    during-run isolation oracles (#2440), and a production-profile scenario with crash
    cycles and transaction-granular durability (#2441).
  • The full Cypher language surface (sprint 346). Access-path parity and
    plan-stability oracles via Explain (#2447); a per-tick QueryCounters oracle over
    the drained result (#2448); relationship SET/REMOVE/DELETE over parallel-edge
    instances (#2449); result-verified range, prefix and IN seeks in both spellings
    (#2450); pattern shapes unlocking the join/intersect planner family (#2451); grouped
    aggregation, DISTINCT, UNION and exact aggregate oracles (#2452); a
    null-semantics scenario that makes the three-valued-logic probes non-tautological
    (#2453); FOREACH write-path equivalence by oracle expansion (#2454); schema
    introspection and full constraint-kind adjudication (#2455); the statistics-planning
    regime through db.stats.refresh (#2456); temporal values stored, recovered and read
    back as temporals (#2457); entity-valued and pure-function probes (#2458);
    list-property predicates and entity map projection (#2459); ordering, pagination and
    multi-part query structure (#2460); the MERGE surface with its semantics pinned as
    measured (#2461); the DELETE contract, notifications, YIELD filter and the wire
    parameter matrix (#2462); and the variable-length path surface, oracle-computed
    (#2463).
  • Storage, durability and the MVCC substrate (sprint 347). DDL across the
    checkpoint and snapshot boundary (#2464); a crash inside the checkpoint publish
    window (#2465); bulk-import publish parity with its uncovered faults documented
    (#2466); a snapshot component-corruption fail-stop battery (#2467); typed values and
    edge properties through the snapshot codec (#2468); MVCC clock and sequence recovery
    across a snapshot boundary (#2469); MVCC substrate telemetry read and adjudicated
    (#2470); group-commit coalescing and its fail-all path (#2471); WAL writer surface
    oracles with a constructed contiguity control (#2472); a key and weight codec matrix
    across crash and upgrade (#2473); transaction-size cap sentinels with the refusal
    proven inert (#2474); store.DB teardown under a cancelled context and concurrent
    closers (#2475); the checkpointer cadence on a fake clock (#2476); cross-release
    compatibility beyond the WAL (#2477); a csrfile access-pattern and weight-kind
    matrix with the alignment gate probed (#2478); required counters per fault scenario
    (#2479); and the graph/io codec surface with cap provocation and reproducibility
    oracles (#2480).
  • The Bolt wire surface (sprint 348). The authentication surface with the WAL as
    witness (#2481); the transaction registry on a fake clock (#2482);
    Server.Shutdown's drain and Closer ordering (#2483); streaming semantics —
    paging, DISCARD and the qid contract (#2484); BEGIN extras — bookmarks,
    tx_timeout, metadata, mode, db and ROUTE (#2485); the protocol version matrix
    with 4.4, 5.0 and 5.x side by side (#2486); and the aggregate inbound-decode budget
    with its two distinct nesting caps (#2487).
  • The graph API, search and bulk-load surfaces (sprint 349). The bulk loader's full
    contract against an independent model (#2488); every public context-accepting
    search entry point under cancellation (#2489); index contents across the snapshot
    boundary (#2490); the lock-free CSR publisher's refcount lifecycle (#2491);
    graph/query as a differential read path against Cypher and an independent oracle
    (#2492); the typed schema as a runtime enforcement hook (#2493); the count store held
    to a model across recovery (#2494); the stateful PageRanker's two published claims
    (#2495); the label index's scoped and range surface against a naive model (#2496);
    and min-cost flow's negative-cost regime with the hoisted-reverse Dijkstra (#2497).
  • The SimDisk crash model was overhauled for fidelity (#2514, #2535, #2536,
    #2537, #2538, #2547), after an audit recorded in
    docs/audits/simdisk-crash-model-fidelity-2026-08-18.md
    with 15 findings, 12 of them filed as tasks. A rename undo log with
    durable-prefix semantics; a durableLen watermark, so unsynced file data is now
    discarded on a host crash
    ; CrashHost and CrashProcess split apart; unlink
    modelled as a non-durable directory mutation; a DirSync fault arm; and ghost
    directory entries pruned. The watermark's own cost was measured against the
    alternative: +0.00 % bytes and allocations per operation, against +97.64 % time
    and +99.94 % bytes for a full clone. The whole catalogue passes under SOAK_FULL=1
    with 36 of 36 scenarios and zero skips.
  • Scenario-scoped required counters (#2479): every fault scenario declares the
    counters its faults must move, so a fault that silently stopped firing fails the run
    instead of passing it.
  • A VACUOUS_RUN violation family (#2614), so a run that never exercised its
    subject is reported as having proved nothing rather than as an ORACLE_DEVIATION.
    About 178 of 818 emitter lines migrated; checkCheckpointStormNonVacuity had to be
    split, because migrating it wholesale would have relabelled a real snapshot loss
    as "the run proved nothing".
  • Cross-release coverage now reaches the snapshot directory (#2477, #2531). The
    harness previously imported only the WAL, recovery and transaction packages, so a
    prior release's snapshot directory had never been opened by current code. The tag
    list grows from two to four — v0.1.0 as the oldest artefact in existence, v0.2.0
    and v0.3.0 as the bracket around the WAL-replay gap, and v0.11.0 because it is
    the release users actually upgrade from. All four publish a manifest-v3 snapshot
    that current code opens snapshot-only, with 52 labels an...
Read more