Documentation
¶
Overview ¶
Package adjlist provides a mutable, sharded adjacency-list backend for the gograph module.
AdjList is the canonical builder used to assemble a graph incrementally before it is frozen into an immutable CSR view for analytics. It supports directed and undirected graphs (mirrored insertion), parallel edges (multigraph mode), and self-loops.
Storage and concurrency ¶
Storage is split into 256 independently locked shards aligned with the graph.Mapper's sharding (the low 8 bits of every NodeID identify the shard). Within a shard, adjacency entries are indexed by the intra-shard component of the NodeID — a direct slice access with no map lookup.
Reads (HasEdge, Neighbours, Order, Size) are lock-free: a reader performs a single atomic load on the shard's slot slice, a single atomic load on the slot itself, and operates on the immutable snapshot of neighbours/weights stored in the resulting entry.
Writes (AddEdge, RemoveEdge) take the shard mutex, copy the entry's slices with the modification applied, and publish the new entry pointer via sync/atomic.StorePointer. Compact likewise takes the shard mutex and republishes each slack-bearing entry with its backing arrays right-sized to exact length. Concurrent readers always observe a consistent snapshot — either the entry before the write or the entry after it.
The yield callback of AdjList.Neighbours iterates over slices owned by a snapshotted entry and may safely call any other AdjList operation; no locks are held during yield.
Index ¶
- Variables
- type AdjList
- func (a *AdjList[N, W]) AddEdge(src, dst N, w W) error
- func (a *AdjList[N, W]) AddEdgeH(src, dst N, w W, handle uint64) error
- func (a *AdjList[N, W]) AddEdgeLabeled(src, dst N, w W, label uint32) error
- func (a *AdjList[N, W]) AddEdgeLabeledH(src, dst N, w W, handle uint64, label uint32) error
- func (a *AdjList[N, W]) AddEdgeLabeledWithProp(src, dst N, w W, label uint32, payload any) error
- func (a *AdjList[N, W]) AddNode(n N) error
- func (a *AdjList[N, W]) BeginCommit()
- func (a *AdjList[N, W]) BeginExclusiveBuild()
- func (a *AdjList[N, W]) ClearEdgeLabelSlotValue(src, dst graph.NodeID, v uint32) bool
- func (a *AdjList[N, W]) ClearEdgeLabelSlots(src, dst graph.NodeID)
- func (a *AdjList[N, W]) ClearEdgeLabelSlotsValue(src, dst graph.NodeID, v uint32) int
- func (a *AdjList[N, W]) Compact(ctx context.Context)
- func (a *AdjList[N, W]) Config() Config
- func (a *AdjList[N, W]) Directed() bool
- func (a *AdjList[N, W]) DisableVersioning()
- func (a *AdjList[N, W]) EnableVersioning()
- func (a *AdjList[N, W]) EndCommit()
- func (a *AdjList[N, W]) EndExclusiveBuild()
- func (a *AdjList[N, W]) EntryNeighboursAsOf(id graph.NodeID, startTS, txID uint64) []graph.NodeID
- func (a *AdjList[N, W]) EntrySlotLabelsAsOf(id graph.NodeID, startTS, txID uint64) (neighbours []graph.NodeID, labels []uint32)
- func (a *AdjList[N, W]) EntryViewAsOf(id graph.NodeID, startTS, txID uint64) EntryView[W]
- func (a *AdjList[N, W]) EntryViewAsOfVisible(id graph.NodeID, visible func(*mvcc.CommitInfo, uint64) bool) EntryView[W]
- func (a *AdjList[N, W]) HasEdge(src, dst N) bool
- func (a *AdjList[N, W]) HasEdgeAsOf(srcID, dstID graph.NodeID, startTS, txID uint64) bool
- func (a *AdjList[N, W]) InExclusiveBuild() bool
- func (a *AdjList[N, W]) InNeighbourIDs(dst graph.NodeID) []graph.NodeID
- func (a *AdjList[N, W]) InNeighbours(dst N) []N
- func (a *AdjList[N, W]) LoadEntry(id graph.NodeID) (neighbours []graph.NodeID, weights []W)
- func (a *AdjList[N, W]) LoadEntryAux(id graph.NodeID) AuxColumn
- func (a *AdjList[N, W]) LoadEntryH(id graph.NodeID) (neighbours []graph.NodeID, weights []W, handles []uint64)
- func (a *AdjList[N, W]) LoadEntryHAt(id graph.NodeID, at At) (neighbours []graph.NodeID, weights []W, handles []uint64)
- func (a *AdjList[N, W]) LoadEntryLabels(id graph.NodeID) []uint32
- func (a *AdjList[N, W]) LoadEntrySlotLabels(id graph.NodeID) (neighbours []graph.NodeID, labels []uint32)
- func (a *AdjList[N, W]) LoadEntryView(id graph.NodeID) EntryView[W]
- func (a *AdjList[N, W]) Mapper() *graph.Mapper[N]
- func (a *AdjList[N, W]) MaxNodeID() graph.NodeID
- func (a *AdjList[N, W]) Multigraph() bool
- func (a *AdjList[N, W]) Neighbours(src N) iter.Seq2[N, W]
- func (a *AdjList[N, W]) NeighboursAsOf(id graph.NodeID, startTS, txID uint64) []graph.NodeID
- func (a *AdjList[N, W]) NestedServingWindows() uint64
- func (a *AdjList[N, W]) NextHandle() uint64
- func (a *AdjList[N, W]) Order() uint64
- func (a *AdjList[N, W]) OutDegree(src N) (int, bool)
- func (a *AdjList[N, W]) OutDegreeAsOf(id graph.NodeID, startTS, txID uint64) int
- func (a *AdjList[N, W]) OutDegreeByID(srcID graph.NodeID) (int, bool)
- func (a *AdjList[N, W]) OutDegreeByType(src N, relType uint32) (int, bool)
- func (a *AdjList[N, W]) OutDegreeFunc(src N, keep func(dst graph.NodeID, relType uint32) bool) (int, bool)
- func (a *AdjList[N, W]) OutDegreeFuncBounded(src N, limit int, keep func(dst graph.NodeID, relType uint32) bool) (int, bool)
- func (a *AdjList[N, W]) OutDegreeFuncBoundedByID(srcID graph.NodeID, limit int, ...) (int, bool)
- func (a *AdjList[N, W]) PinSnapshot() *Snapshot[N, W]
- func (a *AdjList[N, W]) Reclaim(watermark uint64, hist *mvcc.DepthHist) int
- func (a *AdjList[N, W]) RecordedInEdges() int64
- func (a *AdjList[N, W]) RemoveAllEdgesFrom(src N)
- func (a *AdjList[N, W]) RemoveEdge(src, dst N)
- func (a *AdjList[N, W]) RemoveEdgeByHandle(src, dst N, handle uint64) bool
- func (a *AdjList[N, W]) SeedHandleSeq(highWater uint64)
- func (a *AdjList[N, W]) SetAuxFactory(fn func(length int, payload any) AuxColumn)
- func (a *AdjList[N, W]) SetEdgeLabelSlot(src, dst graph.NodeID, v uint32) bool
- func (a *AdjList[N, W]) SetEdgeLabelSlots(src graph.NodeID, updates map[graph.NodeID]uint32) int
- func (a *AdjList[N, W]) SetEdgeLabelSlotsAt(src, dst graph.NodeID, idxs []int, v uint32) int
- func (a *AdjList[N, W]) SetWriteStamp(s *mvcc.WriteStamp)
- func (a *AdjList[N, W]) Size() uint64
- func (a *AdjList[N, W]) UpdateEntryAux(src graph.NodeID, ...) bool
- func (a *AdjList[N, W]) VersionCount() int64
- func (a *AdjList[N, W]) Weightless() bool
- func (a *AdjList[N, W]) Writer(tx mvcc.Tx) Writer[N, W]
- type At
- type AuxColumn
- type Config
- type EntryView
- type Snapshot
- func (s *Snapshot[N, W]) HasEdge(src, dst N) bool
- func (s *Snapshot[N, W]) LoadEntry(id graph.NodeID) (neighbours []graph.NodeID, weights []W)
- func (s *Snapshot[N, W]) LoadEntryAux(id graph.NodeID) AuxColumn
- func (s *Snapshot[N, W]) LoadEntryH(id graph.NodeID) (neighbours []graph.NodeID, weights []W, handles []uint64)
- func (s *Snapshot[N, W]) LoadEntryLabels(id graph.NodeID) []uint32
- type Writer
- func (wr Writer[N, W]) AddEdge(src, dst N, w W) error
- func (wr Writer[N, W]) AddEdgeH(src, dst N, w W, handle uint64) error
- func (wr Writer[N, W]) AddEdgeLabeled(src, dst N, w W, label uint32) error
- func (wr Writer[N, W]) AddEdgeLabeledH(src, dst N, w W, handle uint64, label uint32) error
- func (wr Writer[N, W]) AddEdgeLabeledWithProp(src, dst N, w W, label uint32, payload any) error
- func (wr Writer[N, W]) ClearEdgeLabelSlotValue(src, dst graph.NodeID, v uint32) bool
- func (wr Writer[N, W]) ClearEdgeLabelSlots(src, dst graph.NodeID)
- func (wr Writer[N, W]) ClearEdgeLabelSlotsValue(src, dst graph.NodeID, v uint32) int
- func (wr Writer[N, W]) RemoveAllEdgesFrom(src N)
- func (wr Writer[N, W]) RemoveEdge(src, dst N)
- func (wr Writer[N, W]) RemoveEdgeByHandle(src, dst N, handle uint64) bool
- func (wr Writer[N, W]) SetEdgeLabelSlot(src, dst graph.NodeID, v uint32) bool
- func (wr Writer[N, W]) SetEdgeLabelSlots(src graph.NodeID, updates map[graph.NodeID]uint32) int
- func (wr Writer[N, W]) SetEdgeLabelSlotsAt(src, dst graph.NodeID, idxs []int, v uint32) int
- func (wr Writer[N, W]) Tx() mvcc.Tx
- func (wr Writer[N, W]) UpdateEntryAux(src graph.NodeID, ...) bool
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrShardFull = errors.New("adjlist: shard capacity exhausted")
ErrShardFull is returned by AdjList.AddNode and AdjList.AddEdge when the shard responsible for a new NodeID would have to grow beyond Config.MaxShardCapacity. Callers inspect this error with errors.Is; the AdjList state is unchanged when this error is returned (no node interned, no edge published, size counter not advanced).
Functions ¶
This section is empty.
Types ¶
type AdjList ¶
type AdjList[N comparable, W any] struct { // contains filtered or unexported fields }
AdjList is a mutable adjacency-list graph generic over the user node type N and the edge weight type W. Construct one with New.
Concurrency: AdjList is safe for any number of concurrent readers (Neighbours, LoadEntry, HasEdge, Order, Size) AND concurrent writers (AddEdge, AddNode, RemoveEdge). The 256-way shard layout serialises only writers landing in the same shard; readers never take a mutex and observe a consistent snapshot via atomic.Pointer.
Bounded growth: when Config.MaxShardCapacity is set, AddEdge and AddNode return ErrShardFull instead of growing the affected shard past the cap. Callers must propagate the error and stop offering new work to the saturated shard; the AdjList state is unchanged when ErrShardFull is returned.
NodeID stability: NodeIDs assigned by the Mapper are monotonically increasing within each shard and are never reused. Removing an edge does not remove the endpoint nodes from the Mapper; their NodeIDs remain valid for the lifetime of the AdjList. Code that caches NodeIDs (e.g., an external CSR snapshot) may rely on them remaining stable as long as the originating AdjList is live.
Example ¶
ExampleAdjList builds a small directed weighted graph and reads back its order (node count), size (edge count), and edge membership. The Config selects the graph variant; here Directed means AddEdge inserts only the forward edge.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
)
func main() {
g := adjlist.New[string, int](adjlist.Config{Directed: true})
// AddEdge auto-creates endpoint nodes; the third argument is the
// edge weight (here an int).
_ = g.AddEdge("a", "b", 10)
_ = g.AddEdge("a", "c", 20)
fmt.Println("order:", g.Order())
fmt.Println("size:", g.Size())
fmt.Println("a->b:", g.HasEdge("a", "b"))
fmt.Println("b->a:", g.HasEdge("b", "a")) // directed: reverse absent
}
Output: order: 3 size: 2 a->b: true b->a: false
Example (Undirected) ¶
ExampleAdjList_undirected shows that an undirected Config mirrors every insertion: AddEdge("a","b") makes both a->b and b->a present.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
)
func main() {
g := adjlist.New[string, int](adjlist.Config{Directed: false})
_ = g.AddEdge("a", "b", 1)
fmt.Println("a->b:", g.HasEdge("a", "b"))
fmt.Println("b->a:", g.HasEdge("b", "a"))
}
Output: a->b: true b->a: true
func New ¶
func New[N comparable, W any](cfg Config) *AdjList[N, W]
New returns an empty AdjList configured by cfg.
func (*AdjList[N, W]) AddEdge ¶
AddEdge inserts a directed edge from src to dst with weight w, also interning the endpoints if they are not yet known. When the graph is undirected, the mirrored edge (dst, src) is inserted as well. Implements graph.Graph.
AddEdge returns ErrShardFull when Config.MaxShardCapacity is set and the responsible shard would have to grow past the cap to store the new entry. In that case no edge is published; the size counter is not advanced. The endpoints may, however, remain interned in the underlying graph.Mapper: callers that need strict orphan-free behaviour should detect ErrShardFull and treat the graph as saturated.
func (*AdjList[N, W]) AddEdgeH ¶
AddEdgeH is AdjList.AddEdge with an explicit, caller-supplied stable edge handle. The handle is stored in the slot's parallel handle column (see [adjEntry.handles]) so the read path can recover per-slot edge identity without inferring it from CSR slot order. The handle is carried verbatim across compaction on AdjList.RemoveEdge: a surviving parallel slot keeps its original handle, and handles are never reused or renumbered.
For an undirected graph the mirrored (dst, src) slot receives the SAME handle, so both directions of one logical edge share one identity.
AddEdgeH honours the same ErrShardFull and all-or-nothing contract as AdjList.AddEdge. In simple-graph mode a duplicate (src, dst) is still a no-op and the supplied handle is ignored (the existing slot keeps its original handle).
func (*AdjList[N, W]) AddEdgeLabeled ¶ added in v0.6.0
AddEdgeLabeled is AdjList.AddEdge with an OPAQUE 4-byte label supplied AT edge-insertion time. The label is written into the slot's parallel label column (see [adjEntry.labels]) within the SAME O(1)-amortised append fast path that writes the neighbour and weight — no separate copy-on-write of the whole column afterwards. This is the bulk-build path for a labelled graph: a degree-d source is assembled in O(d) amortised total, not O(d²).
adjlist treats label as opaque: any uint32 is accepted, including 0 (the higher layer's "no label" sentinel). A label-free graph that never calls this method keeps the labels column nil and pays no extra memory.
For an undirected graph the mirrored (dst, src) slot receives the SAME label, so both directions of one logical edge carry the same relationship type.
AddEdgeLabeled honours the same ErrShardFull and all-or-nothing contract as AdjList.AddEdge. In simple-graph mode a duplicate (src, dst) is still a no-op and the supplied label is ignored (the existing slot keeps its label). Use AdjList.SetEdgeLabelSlot to (re)label a slot of a pre-existing edge.
func (*AdjList[N, W]) AddEdgeLabeledH ¶ added in v0.6.0
AddEdgeLabeledH fuses AdjList.AddEdgeH and AdjList.AddEdgeLabeled: it stores both a caller-supplied stable handle and an opaque label on the new slot within the same append fast path. Both optional columns are written at position oldLen at insertion time, so a labelled, handle-carrying edge is still an O(1)-amortised append.
func (*AdjList[N, W]) AddEdgeLabeledWithProp ¶ added in v0.6.0
AddEdgeLabeledWithProp fuses AdjList.AddEdgeLabeled with one opaque edge-property value written into the new slot's AuxColumn AT append time. Like the label, the property value lands on the new slot at position oldLen within the SAME O(1)-amortised append that writes the neighbour and weight — no separate copy-on-write of the whole aux column afterwards. This is the bulk-build fast path for a property-carrying graph: a degree-d source that stamps one property per edge is assembled in O(d) amortised total, not the O(d²) a per-edge AdjList.UpdateEntryAux copy-on-write would cost.
payload is opaque to adjlist: it is forwarded verbatim to AuxColumn.GrowSlotWithValue when the source already has an aux column, or to the factory registered via AdjList.SetAuxFactory when this is the source's FIRST edge (no column exists yet to grow). A nil aux factory on the fresh-entry path silently drops the payload, so the higher layer must register one before using this method; lpg always does.
For an undirected graph the mirrored (dst, src) slot receives the SAME label (relationship type is symmetric) but NOT the property payload: the aux column is directional, matching AdjList.UpdateEntryAux / the higher layer's SetEdgeProperty, which writes only the source's entry. The higher layer reads a pair's properties from the source's entry, so stamping the payload only on the forward slot reproduces exactly the two-step AddEdgeLabeled + per-source property write it replaces. AddEdgeLabeledWithProp honours the same ErrShardFull and all-or-nothing contract as AdjList.AddEdge. In simple-graph mode a duplicate (src, dst) is still a no-op and neither the label nor the payload is stamped on the existing slot; use AdjList.SetEdgeLabelSlot / AdjList.UpdateEntryAux to mutate a pre-existing edge.
func (*AdjList[N, W]) AddNode ¶
AddNode inserts n if not already present. The node enters the adjacency list lazily on its first outgoing edge; AddNode only interns the value with the Mapper, which is sufficient for AdjList.Order to account for it. Implements graph.Graph.
AddNode never returns ErrShardFull on its own because it does not touch any shard's slot array; the bounded-growth contract becomes observable on the first AddEdge for which the responsible shard would have to grow past Config.MaxShardCapacity. The error return exists to satisfy the graph.Graph contract and to leave room for future implementations that reserve shard storage eagerly.
func (*AdjList[N, W]) BeginCommit ¶ added in v0.6.0
func (a *AdjList[N, W]) BeginCommit()
BeginCommit opens a commit window so that the writes of one transaction clone each touched shard's slot array AT MOST ONCE (on first touch) and mutate that private builder in place for the rest of the window, instead of cloning the whole array on every write. It bounds a multi-op commit's copy-on-write cost to O(distinct shards touched) rather than O(ops). The matching AdjList.EndCommit retires that owner.
Single-writer contract (load-bearing, and now NARROW) ¶
BeginCommit/EndCommit are NOT internally synchronised and MUST be called only by a single writer with NO concurrent writer, NO concurrent reader and NO concurrent AdjList.PinSnapshot for the window's whole duration. The sanctioned callers are provably-exclusive bulk builds — single-threaded WAL recovery replay, or a bulk-ingest loop the concurrent public API cannot reach. Wrapping such a loop in ONE window restores baseline write cost (clone once per shard, then in place).
That contract used to cover the ENGINE too, because the engine bracketed the visibility barrier around every write and was therefore single-writer by construction. It no longer needs to: a transaction with a commit record open on the write stamp is identified by that record, so the dedup follows the transaction rather than the lock, and two concurrent transactions cannot disturb each other's builders (rmp #2301). The shared window state this paragraph used to defend — a depth counter and a dirty-shard list on the AdjList — no longer exists.
Calls nest: a nested BeginCommit/EndCommit pair just adjusts the depth; only the outermost EndCommit retires the owner. A transaction-owned builder needs no depth at all — nested statements of one transaction share its commit record, which is what makes them one transaction.
F3.5 / #1671 — the dedup SURVIVES the lock-free read path ¶
The in-place builder mutation that makes this dedup work was expected to be a barrier-borrowed shortcut that MVCC would have to give up. It is not: with versioning armed the replaced entry stays reachable through the new entry's version chain, so a lock-free reader steps back to it rather than tearing. See the unwind note on [AdjList.storeEntry] for the full argument and the tests that pin it.
What it is for now, after rmp #2301 ¶
A transaction that has a commit record open on the write stamp gets the clone-once dedup WITHOUT calling this at all: the record identifies the transaction, and [adjShard.buildingOwner] uses it directly. That is the engine path, and it is why the dedup is now safe under concurrent writers — the window state that used to be shared (commitDepth, dirtyShards) is gone.
BeginCommit remains for the paths that write with NO transaction open on the stamp and are exclusive by contract: single-threaded WAL recovery replay and bulk ingest. It installs a synthetic owner so those writes are deduped too. Calling it from a path that already has a transaction open is harmless and redundant — the record wins.
BeginCommit is safe to call only as described above; misuse (concurrent callers, or reads not excluded) is undefined.
func (*AdjList[N, W]) BeginExclusiveBuild ¶ added in v0.11.0
func (a *AdjList[N, W]) BeginExclusiveBuild()
BeginExclusiveBuild opens a commit window for a provably-exclusive REBUILD of the adjacency — WAL recovery replaying into a fresh graph, or a bulk import — and asserts the precondition that makes it sound.
Why a distinct entry point (rmp #2302, audit finding E21) ¶
AdjList.BeginCommit and this method do the same thing to the same fields. They differ entirely in what licenses them:
- BeginCommit is called by the serving write path (lpg's ApplyAtomically and LockBarrier) and is safe because the graph's exclusive visibility barrier is held for the whole window.
- This is called by store/recovery and store/bulkimport, which take NO barrier. It was safe because the graph is not reachable by anyone yet — single-threaded replay, no concurrent reader, no concurrent writer.
Until now both called BeginCommit, so that second licence lived only in a comment. The audit's point is that it must not be silently INHERITED once writers overlap at serving time: a path that legitimately needs no barrier during a rebuild must not become a path that quietly needs none while the engine serves. Splitting the entry points makes the two licences distinct at the call site, and the flag makes overlapping them fail loudly instead of corrupting a builder.
What #2304 still has to do, recorded here because it is easy to miss ¶
[AdjList.builderOwner] prefers bulkOwner OVER the writing transaction's own record, deliberately, so a window's token cannot change mid-window. That means the SERVING path's window currently SHADOWS per-transaction ownership: with the barrier gone, two concurrent writers would both present the same bulkOwner and would reuse each other's private, unpublished shard builders. rmp #2304 must therefore retire the serving path's window in favour of a token that travels with the write — lpg already has one in writeCtx.txID — rather than merely deleting visMu around it.
Why only ONE direction is asserted — measured, after getting it wrong ¶
The first version of this also panicked from AdjList.BeginCommit whenever a serving window was opened during a rebuild. That is too strict, and `make ci` said so: recovery's own replay nests one, on the SAME goroutine, on the dominant path.
adjlist.BeginCommit lpg.ApplyAtomically (lpg.go:712) lpg.reclaimAfterDirectWrite (mvcc_gc.go:135) lpg.addNodeInfo (lpg.go:1206) recovery.applyOpCodec (recovery.go:1616)
A replay creates versions fast enough to cross the reclamation threshold, and the sweep runs inside an ApplyAtomically bracket. Three packages failed on it (cypher, examples/04_persistence, examples/24_social_network_cli), so the guard was rejecting correct behaviour.
The hazard is CONCURRENCY, not nesting — a SECOND goroutine writing while the rebuild runs. Telling the two apart needs goroutine identity, which this package does not have: the only structure that knows which goroutine holds a write window is lpg's barrierGuard, and it is `//go:build race || gograph_debug`. So the sound assertion in this direction belongs in lpg, alongside that guard, and is rmp #2304's to add when it retires the serving path's window. Until then the nesting is COUNTED — see AdjList.NestedServingWindows — so the behaviour is observable rather than merely tolerated.
What IS asserted here is the direction that holds unconditionally: an exclusive build must not START inside a serving window, because a rebuild may only run on a graph nobody is serving.
Nested calls are permitted and expected (a replay applies many ops inside one window); the depth is tracked exactly as BeginCommit's is. The matching AdjList.EndExclusiveBuild must be called once per call, by the same goroutine.
Not safe for concurrent use — that is the whole point.
func (*AdjList[N, W]) ClearEdgeLabelSlotValue ¶ added in v0.6.0
ClearEdgeLabelSlotValue clears the opaque label of the FIRST adjacency slot of src whose neighbour is dst AND whose label equals v, publishing a new immutable entry snapshot. It returns true when such a slot was found and cleared. This targets a specific label value so a multigraph pair whose parallel slots carry different labels can drop exactly one of them without disturbing the others. No-op when v is 0, src has no label column, or no dst-matching slot carries v.
Concurrency: copy-on-write, identical to AdjList.SetEdgeLabelSlot; safe for concurrent use.
func (*AdjList[N, W]) ClearEdgeLabelSlots ¶ added in v0.6.0
ClearEdgeLabelSlots clears the opaque label of EVERY adjacency slot of src whose neighbour is dst (all parallel slots in a multigraph), publishing a single new immutable entry snapshot. It is the bulk inverse used when the last edge between an endpoint pair is removed and the higher layer must drop the pair's per-slot labels in lockstep with its own overflow state. No-op (and no allocation) when src has no label column or no dst-matching slot carries a label.
Concurrency: copy-on-write, identical to AdjList.SetEdgeLabelSlot; safe for concurrent use.
func (*AdjList[N, W]) ClearEdgeLabelSlotsValue ¶ added in v0.11.0
ClearEdgeLabelSlotsValue clears the opaque label of EVERY adjacency slot of src whose neighbour is dst AND whose label equals v, publishing a SINGLE new immutable entry snapshot. It returns the number of slots cleared.
It is the all-slots counterpart of the first-match AdjList.ClearEdgeLabelSlotValue, and the exact inverse of AdjList.SetEdgeLabelSlotsAt for a pair whose parallel slots all carry v: detaching a relationship type from a multigraph pair must leave no slot still carrying it, which one first-match clear cannot guarantee. It targets a specific value, so parallel slots carrying OTHER labels are untouched.
No-op (and no allocation) when v is 0, src has no label column, or no dst-matching slot carries v.
Concurrency: copy-on-write, identical to AdjList.SetEdgeLabelSlot; safe for concurrent use.
func (*AdjList[N, W]) Compact ¶
Compact right-sizes every adjacency entry's backing arrays to their exact live length, reclaiming the slack left by geometric (×2) append growth. After a bulk build a degree-d hub typically over-allocates its neighbours/weights (and optional handles/labels) columns by up to ~2×; across a graph the wasted capacity averages ≈21% of the adjacency arrays. Compact walks every shard, and for each occupied slot whose entry has any column with spare capacity (cap > len) it builds a fresh entry whose every column is allocated at EXACT length (cap == len), copying only the live [0:len] data, then publishes it via the same atomic store-pointer mechanism the writers use. Entries with no slack (every column already cap == len) are skipped to avoid useless churn.
Compact is best run once after a build-then-query workload has finished mutating the graph and before the read-heavy query phase, so the resident footprint reflects the tight arrays.
Concurrency: Compact is safe for concurrent use with lock-free readers. It takes one shard mutex at a time (never two simultaneously, so it can never participate in a lock-ordering cycle with the two-lock cross-shard [AdjList.addEdge] path) and publishes each trimmed entry with sync/atomic.StorePointer. A reader holding the prior entry pointer keeps iterating the old, untrimmed — but never mutated — backing arrays; a reader that loads after the store observes the trimmed entry. No reader ever sees a torn or half-trimmed entry. The trimmed entry preserves the nil-vs-empty distinction of the optional handles/labels columns exactly: a nil column stays nil (a label- or handle-free graph never gains a zero-length slice).
Compact honours ctx cancellation between shards, so a cancelled Compact of a very large graph stops promptly with whatever shards it has already trimmed left consistently published.
func (*AdjList[N, W]) Config ¶ added in v0.2.0
Config returns the Config the AdjList was constructed with. The configuration is fixed at New and never mutated thereafter, so Config is safe to call concurrently with any other operation and always returns the same value for the lifetime of the AdjList. It is used by the snapshot writer to persist the originating graph's directed/multigraph shape so recovery can reconstruct the same variant instead of guessing.
func (*AdjList[N, W]) DisableVersioning ¶ added in v0.11.0
func (a *AdjList[N, W]) DisableVersioning()
DisableVersioning disarms adjacency versioning, so writes record no versions and reads take the current entry with no walk.
It exists so both arms can be compared in ONE process rather than across two builds. Must be called before any edge is written and never concurrently with another operation.
Not safe for concurrent use.
func (*AdjList[N, W]) EnableVersioning ¶ added in v0.11.0
func (a *AdjList[N, W]) EnableVersioning()
EnableVersioning arms adjacency versioning.
Off by default and armed by nothing in the module: the phase lands the mechanism and its measurements, not a behaviour change. Must be called before any edge is written and never concurrently with another operation.
Not safe for concurrent use.
func (*AdjList[N, W]) EndCommit ¶ added in v0.6.0
func (a *AdjList[N, W]) EndCommit()
EndCommit closes the innermost commit window opened by AdjList.BeginCommit. On the OUTERMOST close (depth returns to 0) it freezes every shard touched during the window: it clears each dirty shard's private builder so the already-published slot array becomes immutable (no further in-place write can reach it), and resets the dirty list. Nested closes only decrement the depth.
The published slotsRef of each touched shard already reflects every write of the window (storeEntry published the builder on first touch), so EndCommit performs no additional atomic store — it only relinquishes the right to mutate the builders in place. EndCommit must be called by the same writer that called BeginCommit, exactly once per BeginCommit, under the same exclusive section.
func (*AdjList[N, W]) EndExclusiveBuild ¶ added in v0.11.0
func (a *AdjList[N, W]) EndExclusiveBuild()
EndExclusiveBuild closes the innermost window opened by AdjList.BeginExclusiveBuild. On the outermost close it clears the exclusive-build flag, so the graph becomes available to the serving write path.
func (*AdjList[N, W]) EntryNeighboursAsOf ¶ added in v0.11.0
EntryNeighboursAsOf returns the out-neighbours of id as they were at startTS for a reader running as txID, or nil when the node had none.
The returned slice aliases an immutable entry and MUST NOT be mutated. It is the versioned counterpart of the ordinary neighbour read, and exists so the layer above can be tested against a reconstructed past before any operator depends on it.
Safe for concurrent use.
func (*AdjList[N, W]) EntrySlotLabelsAsOf ¶ added in v0.11.0
func (a *AdjList[N, W]) EntrySlotLabelsAsOf(id graph.NodeID, startTS, txID uint64) (neighbours []graph.NodeID, labels []uint32)
EntrySlotLabelsAsOf is AdjList.LoadEntrySlotLabels as the entry stood at startTS for a reader running as txID.
Safe for concurrent use.
func (*AdjList[N, W]) EntryViewAsOf ¶ added in v0.11.0
EntryViewAsOf returns every column of id's adjacency entry as it was at startTS for a reader running as txID.
The fast path is one atomic load plus one uncontended atomic gate read, which is what a non-versioned read already costs; the chain walk runs only for a node a concurrent writer has actually touched.
Safe for concurrent use.
func (*AdjList[N, W]) EntryViewAsOfVisible ¶ added in v0.11.0
func (a *AdjList[N, W]) EntryViewAsOfVisible(id graph.NodeID, visible func(*mvcc.CommitInfo, uint64) bool) EntryView[W]
EntryViewAsOfVisible is AdjList.EntryViewAsOf through a pinned verdict.
func (*AdjList[N, W]) HasEdge ¶
HasEdge reports whether an edge from src to dst is present. HasEdge is lock-free and allocation-free on the hot path. Implements graph.Graph.
func (*AdjList[N, W]) HasEdgeAsOf ¶ added in v0.11.0
HasEdgeAsOf reports whether a directed edge srcID→dstID existed at startTS for a reader running as txID.
Safe for concurrent use.
func (*AdjList[N, W]) InExclusiveBuild ¶ added in v0.11.0
InExclusiveBuild reports whether an exclusive rebuild window is open.
It exists so a caller that must not run against a half-rebuilt graph can say so in a test or an assertion rather than assuming.
func (*AdjList[N, W]) InNeighbourIDs ¶ added in v0.11.0
InNeighbourIDs returns the distinct NodeIDs holding an edge into dst, excluding dst itself, in graph.Mapper.Walk order. The result is a fresh slice the caller owns; a node with no incoming edge returns nil.
For an undirected graph every edge is stored in both directions, so the answer is the node's neighbour set.
InNeighbourIDs is safe for concurrent use, and takes only the destination's own reverse shard lock: it neither blocks nor is blocked by adjacency operations on other nodes.
func (*AdjList[N, W]) InNeighbours ¶ added in v0.11.0
func (a *AdjList[N, W]) InNeighbours(dst N) []N
InNeighbours returns the keys of the distinct nodes holding an edge into dst, excluding dst itself. Keys that the Mapper can no longer resolve are skipped.
InNeighbours is safe for concurrent use.
func (*AdjList[N, W]) LoadEntry ¶
LoadEntry returns immutable snapshots of the neighbours and parallel weights of the node identified by id, or (nil, nil) if id has no outgoing edges. The returned slices are owned by the current adjacency snapshot and must not be mutated by the caller.
For a weightless graph (see Config.Weightless) the weights return is always nil even when neighbours is non-empty; callers that index it positionally must nil-check and treat an absent weight as the zero value of W.
func (*AdjList[N, W]) LoadEntryAux ¶ added in v0.6.0
LoadEntryAux returns the opaque AuxColumn currently attached to the adjacency entry of the node identified by id, or nil when id has no outgoing edges or no caller has attached an aux column via AdjList.UpdateEntryAux. The returned column is part of the current immutable adjacency snapshot and must not be mutated by the caller; the higher layer reads it lock-free. To align positionally with the column, read the neighbours from the SAME logical snapshot via AdjList.LoadEntry and bound any per-slot scan by the shorter of the two lengths (a concurrent writer may publish a longer neighbours snapshot after the column is loaded).
LoadEntryAux is lock-free and safe for concurrent use.
func (*AdjList[N, W]) LoadEntryH ¶
func (a *AdjList[N, W]) LoadEntryH(id graph.NodeID) (neighbours []graph.NodeID, weights []W, handles []uint64)
LoadEntryH returns immutable snapshots of the neighbours, parallel weights, and parallel stable handles of the node identified by id. The handles slice is nil when this graph carries no per-slot handles (no caller ever used AdjList.AddEdgeH for id); when non-nil it is the same length as neighbours and handles[i] is the stable handle of the edge to neighbours[i]. The returned slices are owned by the current adjacency snapshot and must not be mutated by the caller.
For a weightless graph (see Config.Weightless) the weights return is always nil even when neighbours is non-empty; the CSR builder relies on this to skip the weights array (see graph/csr.BuildFromAdjList).
func (*AdjList[N, W]) LoadEntryHAt ¶ added in v0.11.0
func (a *AdjList[N, W]) LoadEntryHAt( id graph.NodeID, at At, ) (neighbours []graph.NodeID, weights []W, handles []uint64)
LoadEntryHAt is AdjList.LoadEntryH resolving the entry at the instant at names, rather than always at the current one.
It exists so a bulk scan — the CSR build (rmp #2293) — can be written once and select its instant with a loop-invariant branch, instead of paying an extra call frame per node to a wrapper above this layer. That wrapper measured +15.84% on the build; folding the branch in here keeps the call count per node at exactly what LoadEntryH costs.
Safe for concurrent use.
func (*AdjList[N, W]) LoadEntryLabels ¶ added in v0.6.0
LoadEntryLabels returns an immutable snapshot of the optional per-slot label column of the node identified by id, or nil when id has no outgoing edges or no caller has ever set a label on any of id's slots. When non-nil the slice is the same length as the neighbours returned by AdjList.LoadEntry and labels[i] is the OPAQUE label value of the edge to neighbours[i] (0 means "no label on that slot"). adjlist never interprets the value; the higher layer owns its meaning. The returned slice is owned by the current adjacency snapshot and must not be mutated by the caller.
func (*AdjList[N, W]) LoadEntrySlotLabels ¶ added in v0.11.0
func (a *AdjList[N, W]) LoadEntrySlotLabels(id graph.NodeID) (neighbours []graph.NodeID, labels []uint32)
LoadEntrySlotLabels returns id's CURRENT neighbour and per-slot label columns, resolved from ONE entry so the two agree.
It exists beside AdjList.LoadEntryView because the label-resolution path wants exactly these two columns and nothing else, and returning the full five-field view there cost 21.7 ns per call — over half the whole read — purely in copying slice headers the caller discards. Measured; see BenchmarkEdgeSideRead_LabelsByID.
Safe for concurrent use.
func (*AdjList[N, W]) LoadEntryView ¶ added in v0.11.0
LoadEntryView returns every column of id's CURRENT adjacency entry, resolved from one atomic load so the columns are mutually consistent.
It is what a writer inside the visibility barrier reads: the current value, including its own not-yet-published work.
Safe for concurrent use.
func (*AdjList[N, W]) Mapper ¶
Mapper returns the underlying graph.Mapper that translates between user-facing N values and compact NodeIDs.
func (*AdjList[N, W]) MaxNodeID ¶
MaxNodeID returns one more than the largest graph.NodeID that has been assigned by the underlying Mapper. The value is a stable upper bound on the NodeID space at the moment of the call and is the natural size of a NodeID-indexed companion array (for example, the CSR offsets array).
func (*AdjList[N, W]) Multigraph ¶
Multigraph reports whether parallel edges are allowed.
func (*AdjList[N, W]) Neighbours ¶
Neighbours returns an iterator over the live out-neighbours of src and the weight of each connecting edge. The iterator captures a consistent immutable snapshot of src's adjacency at the time of the first call; concurrent mutations of src after that point do not affect this iteration. Implements graph.Graph.
For a weightless graph (see Config.Weightless) the entry carries no weights column, so every neighbour is yielded with the zero value of W — the all-zero "unweighted" representation.
Example ¶
ExampleAdjList_Neighbours iterates the out-neighbours of a node with the Go 1.23 range-over-func form, yielding each neighbour together with its edge weight. Iteration order is unspecified.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
)
func main() {
g := adjlist.New[string, int](adjlist.Config{Directed: true})
_ = g.AddEdge("a", "b", 10)
_ = g.AddEdge("a", "c", 20)
for n, w := range g.Neighbours("a") {
fmt.Printf("a -> %s (weight %d)\n", n, w)
}
}
Output: a -> b (weight 10) a -> c (weight 20)
func (*AdjList[N, W]) NeighboursAsOf ¶ added in v0.11.0
NeighboursAsOf returns the out-neighbours of id as they were at startTS, or nil when it had none. The returned slice aliases an immutable entry and MUST NOT be mutated.
It is AdjList.EntryNeighboursAsOf under the name the rest of the versioned surface uses; both are kept because the older one is already referenced by its own tests.
Safe for concurrent use.
func (*AdjList[N, W]) NestedServingWindows ¶ added in v0.11.0
NestedServingWindows reports how many serving commit windows have been opened while an exclusive build was in progress.
It exists because that nesting is legitimate but load-bearing: it is recovery's own reclamation sweep, and if it ever stopped happening the reclamation debt would be accumulating through a whole replay with nothing draining it. A counter makes it observable; see AdjList.BeginExclusiveBuild for the measured stack.
func (*AdjList[N, W]) NextHandle ¶ added in v0.11.0
NextHandle mints a fresh stable edge handle, never zero and never reused.
It is exported because the layers above must be able to mint a handle BEFORE the adjacency write, so the same value can be written to the WAL and stamped onto the slot: recovery then re-stamps the recorded handle verbatim through AdjList.AddEdgeH and a relationship keeps its identity across a restart.
Safe for concurrent use.
func (*AdjList[N, W]) Order ¶
Order returns the number of distinct nodes currently referenced. The count is read from the underlying graph.Mapper, which acts as the authoritative registry. Order is O(shardCount) and intended for occasional inspection rather than hot-path use. Implements graph.Graph.
func (*AdjList[N, W]) OutDegree ¶ added in v0.11.0
OutDegree returns the number of live out-neighbours of src — exactly the number of pairs AdjList.Neighbours would yield for it — without materialising or iterating them beyond the count. ok is false when src is not interned; a node with no adjacency entry reports (0, true).
What it counts ¶
The count is the length of src's adjacency column, and that is exactly what Neighbours yields: the mapper is append-only (its Resolve fails only for an id that was never interned), so the resolve check Neighbours performs per slot can never reject an id the adjacency holds. Should the mapper ever gain removal, this method and Neighbours must change together — the invariant they share is that a degree equals the number of rows the equivalent expansion produces.
In a multigraph each parallel edge counts once, because each occupies its own adjacency slot. A self-loop counts once.
TOMBSTONES ARE NOT FILTERED HERE, because Neighbours does not filter them either: tombstoning is an lpg-layer concept and the adjacency knows nothing of it. A caller that needs live-node semantics uses the lpg wrapper (lpg.Graph.OutDegree), which applies the same tombstone gate lpg's own traversal applies.
For an UNDIRECTED graph this is the node's full degree: AdjList.AddEdge mirrors the insertion, so a node's adjacency already holds every incident edge. For a DIRECTED graph it is the out-degree only — the adjacency appends forward edges alone, so in-degree is not an adjacency-local quantity. In-edge enumeration is served by the reverse CSR (github.com/FlavioCFOliveira/GoGraph/graph/csr.CSR.BuildReverse), which is built on demand by the query layer; asking this method for it would mean scanning the whole graph, so it does not offer to.
Cost ¶
O(1) in the graph size and in the node's degree: the count is the length of one contiguous column, reached by a single atomic load, with no allocation and no neighbour resolution. That is why this exists — a degree-answerable predicate should not pay for enumeration.
Concurrency ¶
Safe for concurrent use with readers and writers, and lock-free: it reads the same atomically-published immutable entry Neighbours reads. The result is a consistent snapshot of src's adjacency at the moment of the load; a concurrent mutation of src is either fully included or fully absent, never partially observed.
func (*AdjList[N, W]) OutDegreeAsOf ¶ added in v0.11.0
OutDegreeAsOf returns how many outgoing slots id had at startTS.
Safe for concurrent use.
func (*AdjList[N, W]) OutDegreeByID ¶ added in v0.11.0
OutDegreeByID is AdjList.OutDegree keyed by an already-resolved graph.NodeID instead of by the node value. ok is false only when srcID has no adjacency shard slot, which for an id obtained from this graph's mapper cannot happen; a node with no outgoing edges reports (0, true).
It exists because the query layer already holds ids. Going through the value-keyed form would force it to Resolve the id back to a node value and then Lookup that value again — an array read plus a string hash, per call. Measured on the degree-rewrite path (rmp #2232), that round-trip was the dominant remaining cost once the inner plan was gone: 0.426 µs per outer row above the bare-predicate floor, against 0.079 µs for a plain property predicate.
Cost ¶
O(1): one atomic load and a slice length. No allocation, no mapper access.
Concurrency ¶
Safe for concurrent use with readers and writers, and lock-free, on the same terms as AdjList.OutDegree.
func (*AdjList[N, W]) OutDegreeByType ¶ added in v0.11.0
OutDegreeByType returns the number of live out-neighbours of src reached by an edge whose stored relationship-type label equals relType, and is otherwise AdjList.OutDegree. ok is false when src is not interned.
relType IS THE RAW STORED SLOT VALUE, not a higher layer's label id. The adjacency is label-agnostic: it stores whatever uint32 the caller passed to AdjList.AddEdgeLabeled and compares it verbatim. The lpg layer encodes its LabelID with an offset so that 0 can mean "no label", so an lpg caller must pass the ENCODED value — passing a raw LabelID silently counts a different relationship type. See lpg.Graph.OutDegreeByType, which is the method an application should normally use.
An entry with no labels column carries no typed edges, so the count is 0: a graph built without labelled edges has no edge of any type.
Cost ¶
O(d) in the node's degree, not O(1): the labels column has to be read to decide which slots match. It is still free of allocation and of neighbour resolution, and it never touches the graph beyond one node's columns.
func (*AdjList[N, W]) OutDegreeFunc ¶ added in v0.11.0
func (a *AdjList[N, W]) OutDegreeFunc(src N, keep func(dst graph.NodeID, relType uint32) bool) (int, bool)
OutDegreeFunc returns the number of src's out-edges for which keep reports true, without materialising the neighbour set. keep receives each edge's destination NodeID and its relationship-type label (0 when the entry carries no labels column). ok is false when src is not interned.
It exists so a caller that must apply its own liveness or type predicate — the lpg layer's tombstone gate, for instance — still walks src's columns exactly once, with no allocation and no neighbour resolution, instead of iterating AdjList.Neighbours and resolving every node key it does not need.
The slots walked are precisely those Neighbours yields, so a predicate that always returns true gives the same answer as AdjList.OutDegree.
Cost ¶
O(d) in the node's degree, plus the cost of keep. Use AdjList.OutDegree when no predicate is needed; that path is O(1).
Concurrency ¶
Safe for concurrent use with readers and writers, and lock-free: it reads one atomically-published immutable entry, so keep observes a consistent snapshot.
func (*AdjList[N, W]) OutDegreeFuncBounded ¶ added in v0.11.0
func (a *AdjList[N, W]) OutDegreeFuncBounded(src N, limit int, keep func(dst graph.NodeID, relType uint32) bool) (int, bool)
OutDegreeFuncBounded is AdjList.OutDegreeFunc with an early exit: it stops walking as soon as limit edges have been kept, and returns min(trueKeptCount, limit). ok is false when src is not interned.
It exists so a caller comparing a degree against a small literal — "does this node have more than two :KNOWS edges?" — does not walk a supernode to the end to answer it. That is the short-circuit Neo4j's HasDegree family provides over its plain GetDegree (rmp #2232); the untyped degree needs none, being O(1) already, so this is used only where a per-edge predicate forces a walk.
A non-positive limit returns 0 without invoking keep at all, which is the correct answer for "at most zero": the caller has already decided the comparison cannot need more.
Cost ¶
O(min(d, limit)) in the node's degree, plus the cost of keep. No allocation.
Concurrency ¶
Safe for concurrent use with readers and writers, and lock-free, on the same terms as AdjList.OutDegreeFunc.
func (*AdjList[N, W]) OutDegreeFuncBoundedByID ¶ added in v0.11.0
func (a *AdjList[N, W]) OutDegreeFuncBoundedByID(srcID graph.NodeID, limit int, keep func(dst graph.NodeID, relType uint32) bool) (int, bool)
OutDegreeFuncBoundedByID is AdjList.OutDegreeFuncBounded keyed by an already-resolved graph.NodeID. See AdjList.OutDegreeByID for why the id-keyed form exists.
func (*AdjList[N, W]) PinSnapshot ¶ added in v0.6.0
PinSnapshot captures the current per-shard adjacency versions and returns an immutable Snapshot over them. It performs one atomic load per shard (shardCount loads total) and allocates a single Snapshot; it takes no lock and never blocks a writer. A nil shard version (a shard that has never been written) is captured as nil and read back as an empty adjacency, so an empty or partially-populated graph is handled without special-casing.
PinSnapshot is lock-free and safe for concurrent use. The returned Snapshot is valid until it is dropped; the captured versions are kept alive by the reference and reclaimed by GC afterwards.
func (*AdjList[N, W]) Reclaim ¶ added in v0.11.0
Reclaim frees every adjacency version that no reader can reach any more, and returns how many records were released.
watermark is the oldest start timestamp among active readers, from mvcc.Horizon.Oldest. A version superseded at or before it is unreachable: every reader began at or after that instant, so every reader resolves to the current entry rather than stepping back through it. A watermark of zero means "reclaim nothing", which is what the horizon reports while a reader could not be registered.
Why this severs rather than unlinks record by record ¶
A chain is ordered newest-first, so the FIRST record reachable from the current entry whose supersede timestamp is at or before the watermark makes every record behind it unreachable too. Storing nil at that point releases the whole tail in one atomic store, and the Go collector frees the records and the old entries they pinned. There is no need to walk to the end, and no window in which a reader sees a chain with a hole in it.
Why it is safe against a concurrent reader ¶
The store is atomic, so a reader traversing the chain sees either the record or nil. Both answers are correct: the watermark says no active reader has a start timestamp old enough to need what is behind that point, so a reader that sees nil stops at the current entry, which is the version it should get anyway.
Why it IS safe against a concurrent writer (rmp #2308) ¶
This said "not safe to run concurrently with itself or with writers", and the second half was pessimistic rather than true. It takes `s.mu.Lock()` on each shard, and [AdjList.storeEntry] — the only writer of an entry's version chain, through [AdjList.linkVersion] — is called under that same lock from every one of its call sites. A version chain never leaves the shard that owns its slot, so severing one under the shard lock excludes the only writer that could be touching it. The barrier the caller used to hold added nothing here.
Verified by reading the call sites rather than by trusting this comment, which is how the correction was found; the sweep that relies on it is [lpg.Graph.sweepUnit].
The depth histogram ¶
hist accumulates the RETAINED depth of every chain this sweep leaves behind — the number of version records a reader arriving now may still step through — or is nil for a caller that does not measure. The count is the sever walk's own loop counter, so measuring it costs a register increment on the sweeper and no second traversal (rmp #2312). The caller resets it; this function only fills it, because a caller that sweeps several stores decides when a distribution begins.
Safe for concurrent use with readers and with writers. NOT safe to run concurrently with itself: two sweeps would walk and sever the same chain.
func (*AdjList[N, W]) RecordedInEdges ¶ added in v0.11.0
RecordedInEdges reports how many in-edge slots the reverse index currently holds, counted on demand across every shard. On a consistent graph it equals AdjList.Size for a directed graph, and twice it for an undirected one, since an undirected edge is stored in both directions.
It exists so tests can assert the index has not drifted from the forward adjacency — the failure mode that would matter, because an index missing an edge would let DETACH DELETE leave that edge behind. It is O(nodes) and takes every shard lock in turn, so it belongs in a test or a diagnostic, never on a hot path; that is also why it is not maintained as a counter, which would put a shared cache line on the write path to serve an assertion.
func (*AdjList[N, W]) RemoveAllEdgesFrom ¶ added in v0.3.0
func (a *AdjList[N, W]) RemoveAllEdgesFrom(src N)
RemoveAllEdgesFrom removes all edges incident from src in O(d) time for a degree-d hub, instead of the O(d²) cost of d sequential AdjList.RemoveEdge calls.
For directed graphs the method zeroes src's adjacency slot atomically and decrements the edge counter by the number of removed edges. For undirected graphs it additionally removes the mirror entry (src from each dst's list) with one removeOneEdge call per neighbour; those calls are each O(degree-of- dst), which is O(1) for typical star topologies.
Concurrent readers observe either the full pre-deletion state or the post- deletion state; no partial state is ever visible (the src slot is published atomically, and each mirror removal is a separate atomic store).
RemoveAllEdgesFrom is safe for concurrent use.
func (*AdjList[N, W]) RemoveEdge ¶
func (a *AdjList[N, W]) RemoveEdge(src, dst N)
RemoveEdge removes the directed edge from src to dst if present. For multigraphs only one occurrence is removed per call. The endpoints remain in the graph. Implements graph.Graph.
For undirected multigraphs, after removing the first-match slot from the forward direction, the mirror is removed by handle identity (when the removed slot carried a non-zero handle). This ensures that — even after concurrent parallel adds that may have reshuffled slot positions relative to what they were at creation time — the same logical edge is retired from both directions. When no handle is present (plain AddEdge path) the mirror falls back to first-match behaviour, which is correct in the single-writer case that plain AddEdge implies.
func (*AdjList[N, W]) RemoveEdgeByHandle ¶ added in v0.9.0
RemoveEdgeByHandle removes the single directed-edge slot from src to dst whose stable handle equals handle, leaving every sibling parallel slot (and its handle) in place. It returns true when a matching slot was removed and false when no src→dst slot carries handle (already removed, wrong handle, or unknown endpoint) — the caller uses the result to gate side effects (edge counters, transaction-undo records) so a no-op removal records nothing.
This is the instance-precise counterpart of AdjList.RemoveEdge, which removes the FIRST src→dst slot regardless of identity: a Cypher DELETE of a specifically-bound parallel-edge instance must retire the EXACT slot the instance was bound to, not the lowest-indexed occurrence (rmp #2018). For an undirected multigraph the mirror (dst→src) slot carrying the same handle is removed too, so both directions of the one logical edge are retired even after concurrent parallel adds reshuffled slot positions. The edge counter is decremented once for the logical edge.
RemoveEdgeByHandle is safe for concurrent use.
func (*AdjList[N, W]) SeedHandleSeq ¶ added in v0.11.0
SeedHandleSeq raises the handle counter so the next mint is above every handle already present, and is the recovery seam for invariant I5: post-recovery edge creation must never re-mint a handle a restored edge already carries.
It only ever raises. A lower value is ignored rather than rejected, so a caller folding a maximum over several sources — the snapshot's handle column, then the WAL tail — can call it once per source in any order.
Safe for concurrent use.
func (*AdjList[N, W]) SetAuxFactory ¶ added in v0.6.0
SetAuxFactory registers the constructor the fused property-carrying append path uses to build the higher layer's opaque AuxColumn for the FIRST edge of a node (see the auxFactory field). It must be called once, before any fused AdjList.AddEdgeLabeledWithProp that may create a node's first edge, and is the seam that lets adjlist stay oblivious to the column's concrete type: the factory is owned by the higher layer (lpg) and produces a single-slot block carrying the supplied payload at slot 0.
SetAuxFactory is not safe to call concurrently with itself or with fused appends; it is part of one-time wiring at graph construction.
func (*AdjList[N, W]) SetEdgeLabelSlot ¶ added in v0.6.0
SetEdgeLabelSlot stores the opaque label value v on the first adjacency slot of src whose neighbour is dst, publishing a new immutable entry snapshot. It returns true when such a slot was found and updated, false when src has no edge to dst (the caller's higher layer is responsible for the no-live-edge case via its own overflow store).
adjlist treats v as opaque: any uint32 is accepted, including 0 (which the higher layer uses as the "no label" sentinel, so passing 0 clears the slot's label). When src's entry carries no label column yet, one is allocated lazily, length-aligned with neighbours, with every other slot at 0; a label-free graph therefore never pays for the column.
Cost: one call is O(degree(src)) — it scans src's neighbours for the slot and copies the whole label column to publish the change. A loop that re-labels every one of a hub's d slots one call at a time is therefore O(d²); use AdjList.SetEdgeLabelSlots to re-label many slots in a single O(d) copy-on-write publication instead.
Concurrency: the update is copy-on-write. The label column (and the entry) is copied with the change applied and published via the same atomic store-pointer mechanism as AdjList.AddEdgeH; the slot's existing index is never mutated in place, so a concurrent lock-free reader holding the prior snapshot is unaffected. SetEdgeLabelSlot is safe for concurrent use.
func (*AdjList[N, W]) SetEdgeLabelSlots ¶ added in v0.6.0
SetEdgeLabelSlots stores opaque label values on many of src's adjacency slots in a SINGLE copy-on-write publication. For each (neighbour, value) pair in updates it writes value to the FIRST slot of src whose neighbour matches — the same first-slot rule as AdjList.SetEdgeLabelSlot — and returns the number of slots written. A zero value clears that slot's label, exactly as SetEdgeLabelSlot(…, 0) does. Neighbours in updates that src has no slot for are ignored.
This is the amortized bulk form: the whole label column is copied at most once and the new entry is published once, regardless of how many slots are written. Re-labelling all d outgoing slots of a hub therefore costs O(d), versus the O(d²) of d separate AdjList.SetEdgeLabelSlot calls — each of which copies the entire column. Prefer this method for bulk or post-build re-labelling. The column is allocated lazily, so a call that matches no neighbour neither allocates nor publishes.
Concurrency: copy-on-write, identical to AdjList.SetEdgeLabelSlot. The existing column is never mutated in place — a fresh column is published via the same atomic store-pointer mechanism — so a concurrent lock-free reader holding the prior snapshot is unaffected. Safe for concurrent use.
func (*AdjList[N, W]) SetEdgeLabelSlotsAt ¶ added in v0.11.0
SetEdgeLabelSlotsAt stores the opaque label value v on every adjacency slot of src whose index appears in idxs AND whose neighbour is (still) dst, publishing a SINGLE new immutable entry snapshot. It returns the number of slots written.
It is the per-slot counterpart of the first-match AdjList.SetEdgeLabelSlot: the caller has already decided WHICH of a pair's parallel slots are to carry the label and passes their indexes, so a multigraph pair whose parallel slots must all carry the same relationship type is labelled in one copy-on-write publication rather than one per slot. The higher layer needs that choice because the eligibility rule is its own — lpg skips slots whose type is recorded against a per-edge handle — and cannot be expressed here, where the label is opaque.
The dst re-check is not redundant. The caller selects the indexes from an adjacency snapshot read WITHOUT this shard's lock, and a concurrent AdjList.RemoveEdge compacts the neighbour slice, so an index chosen a moment ago may now address a different neighbour. Verifying the neighbour under the lock makes a stale index a skipped write rather than a label stamped on the wrong edge. Indexes out of range are skipped for the same reason.
Cost: O(degree(src)) for the column copy plus O(len(idxs)) for the writes, regardless of how many slots are written — versus the O(d²) of len(idxs) separate AdjList.SetEdgeLabelSlot calls, each of which copies the whole column. The column is allocated lazily, so a call that writes no slot neither allocates nor publishes.
Concurrency: copy-on-write, identical to AdjList.SetEdgeLabelSlot; safe for concurrent use.
func (*AdjList[N, W]) SetWriteStamp ¶ added in v0.11.0
func (a *AdjList[N, W]) SetWriteStamp(s *mvcc.WriteStamp)
SetWriteStamp supplies the mvcc.WriteStamp this AdjList's version records draw from, or nil to draw from none.
It is SHARED rather than owned: the higher layer passes the same stamp to every versioned store it has, so one transaction's topology, node labels and node properties all take one commit record and become visible together. It is a field rather than a parameter because storeEntry has a dozen callers and every one of them would otherwise have to thread it.
What a concurrent unstamped writer inherits, and why that is sound ¶
The public Go-API mutators are documented as per-operation atomic, not transactional, so one may run on another goroutine while a transaction holds the barrier and has the stamp armed. That write then joins the transaction's visibility group: it becomes visible when the transaction publishes rather than the instant it is made. That is not a regression — under the barrier such a write is ALREADY invisible to every barrier reader until the barrier is released, which is the same instant. It cannot be lost, because a transaction's record is published on rollback as well as on commit (the in-memory undo log restores the stored value physically, so the chain nets out; see lpg's endWrite).
Must be called before any edge is written and never concurrently with another operation.
Not safe for concurrent use.
func (*AdjList[N, W]) Size ¶
Size returns the number of edges currently in the graph. For an undirected graph each AddEdge call is counted once; the mirrored neighbour entry is stored but not double-counted. In multigraph mode every parallel edge counts. Implements graph.Graph.
func (*AdjList[N, W]) UpdateEntryAux ¶ added in v0.6.0
func (a *AdjList[N, W]) UpdateEntryAux( src graph.NodeID, fn func(cur AuxColumn, neighbours []graph.NodeID) (AuxColumn, bool), ) bool
UpdateEntryAux atomically replaces the opaque AuxColumn of src's adjacency entry under the shard write lock, using the caller-supplied transform fn. fn receives the entry's CURRENT aux column (nil when none has been attached yet) and an immutable snapshot of its neighbours, and returns the NEW aux column plus a bool reporting whether anything changed. When fn reports false the entry is left untouched and no new snapshot is published; when it reports true a fresh immutable [adjEntry] sharing the unchanged neighbours / weights / handles / labels headers and carrying the returned aux is published via the same atomic store-pointer mechanism the writers use.
UpdateEntryAux returns false when src has no adjacency entry (no outgoing edge), in which case fn is not called — the higher layer's contract is that an aux value (an edge property) is only ever attached to a live edge slot, so there is no entry to attach to. It returns the bool fn reported otherwise.
The transform MUST build the new column copy-on-write and return it fully-populated: adjlist publishes it with a single atomic store, so a concurrent lock-free reader observes either the prior column or the new one, never a half-built column. fn runs under the shard write lock and must not call back into any AdjList method that takes the same shard lock.
UpdateEntryAux is safe for concurrent use.
func (*AdjList[N, W]) VersionCount ¶ added in v0.11.0
VersionCount returns the number of live adjacency version records.
The lock-free gate a reader consults before considering a walk, and the memory a reclamation phase owes: nothing reclaims these yet, so under sustained topology churn this grows without bound.
Safe for concurrent use.
func (*AdjList[N, W]) Weightless ¶ added in v0.6.0
Weightless reports whether the graph carries no per-edge weight column (see Config.Weightless). When true, AdjList.LoadEntry and AdjList.LoadEntryH always return a nil weights slice and the weight argument of AdjList.AddEdge is ignored. The CSR builder (graph/csr.BuildFromAdjList) reads this to skip the weights array, so a weightless graph yields a nil-weights CSR that the snapshot writer persists with hasWeights=0. The value is fixed at New and never mutated, so Weightless is safe to call concurrently with any other operation.
type At ¶ added in v0.11.0
At names the instant an entry read resolves at.
The zero value means "the current entry", which is what every pre-MVCC caller wants and what a writer inside the visibility barrier requires: it applies eagerly and must see its own not-yet-published work. Versioned is a separate field rather than a StartTS sentinel because zero is a legitimate timestamp for a reader that started before any commit.
type AuxColumn ¶ added in v0.6.0
type AuxColumn interface {
// GrowSlot returns a new column of length oldLen+1 whose existing slots
// [0,oldLen) are unchanged and whose new slot at index oldLen is ABSENT
// (carries no value — the implementation must clear any presence/validity
// bit for that slot, never reusing a stale value from recycled backing
// storage). oldLen is the neighbour count of the entry BEFORE the append.
GrowSlot(oldLen int) AuxColumn
// GrowSlotWithValue is the value-carrying analogue of [AuxColumn.GrowSlot]:
// it returns a new column of length oldLen+1 whose existing slots [0,oldLen)
// are unchanged and whose new slot at index oldLen is PRESENT, carrying the
// opaque payload. adjlist never interprets payload; it forwards verbatim the
// value the higher layer supplied to the fused append entry point so the
// per-slot value is written during the same O(1)-amortised append that grows
// the entry, with no separate copy-on-write of the whole column afterwards
// (the mechanism that makes a bulk property-carrying build O(degree) per
// source rather than O(degree²)).
//
// The new slot at oldLen MUST be marked present (its validity bit set, or
// for a sparse representation its index appended) so a subsequent read finds
// the value — this is the only structural difference from [AuxColumn.GrowSlot],
// which leaves the slot absent. Because oldLen is strictly greater than every
// existing slot index, an implementation may append the value at the tail of
// its backing (and, for a coordinate-list representation, append oldLen at the
// end of its strictly-ascending index array) in O(1) amortised — never an
// ordered insert.
//
// Like [AuxColumn.GrowSlot] the returned column is a fresh immutable
// copy-on-write value published the same lock-free way; the receiver is never
// mutated. oldLen is the neighbour count of the entry BEFORE the append.
GrowSlotWithValue(oldLen int, payload any) AuxColumn
// CompactSlot returns a new column of length n-1 with the slot at idx
// excised: result slots [0,idx) equal the receiver's [0,idx) and result
// slots [idx,n-1) equal the receiver's [idx+1,n). idx is a valid index in
// [0,n) where n is the receiver's current length.
CompactSlot(idx int) AuxColumn
// Compact returns a column logically equal to the receiver but with its
// internal backing storage right-sized to the live contents, reclaiming any
// slack an amortised-growth build path left behind, or the receiver itself
// when it already holds no slack. It is the column analogue of
// [AdjList.Compact]'s topology-array trimming and is invoked from the same
// pass. An implementation whose representation is always exactly sized (no
// over-allocation) may return the receiver unchanged. The returned column
// must read identically to the receiver and is published the same lock-free
// way, so it must be a fresh immutable value when it differs.
Compact() AuxColumn
}
AuxColumn is an opaque, immutable per-entry side column the higher layer attaches to a node's adjacency entry to carry one logical value per neighbour slot, aligned 1:1 to the entry's neighbours array. adjlist never interprets the contents; it only drives the column's lifecycle so the per-slot alignment is preserved across the two structural slot mutations adjlist performs:
- an APPEND grows the entry by one slot at position oldLen;
- a REMOVE excises one slot at position idx and shifts the tail down.
Both lifecycle methods return a NEW immutable column (copy-on-write); the receiver is never mutated, so a concurrent lock-free reader holding the prior entry (and thus the prior column) is unaffected. An implementation must keep its own length equal to the entry's neighbour count at all observable points.
Concurrency: an AuxColumn value is published as part of an immutable [adjEntry] via atomic.StorePointer and is read lock-free thereafter, so an implementation must be safe for concurrent reads once returned from a lifecycle method. adjlist holds the shard mutex when it calls GrowSlot / CompactSlot, so those calls never race each other for one entry.
type Config ¶
type Config struct {
// MaxShardCapacity, when > 0, caps the number of node-slots that
// any individual shard may grow to. AddNode (or AddEdge that
// would create a new node or store an outgoing entry in the
// responsible shard) returns [ErrShardFull] when growth past the
// cap would otherwise occur; the AdjList state is left unchanged.
// The default (0) places no upper bound — a shard doubles its
// slot slice indefinitely.
MaxShardCapacity int
// Directed, when true, treats AddEdge as a directed insertion. When
// false, AddEdge also inserts the reverse edge (mirrored insertion).
Directed bool
// Multigraph, when true, allows parallel edges between the same
// pair of endpoints; AddEdge always appends. When false (simple
// graph), repeated AddEdge calls on the same endpoint pair are
// idempotent — the existing edge stays and the new weight is
// ignored.
Multigraph bool
// Weightless, when true, builds a graph that carries NO per-edge weight
// payload: the [adjEntry.weights] column is never allocated and stays nil
// for every node, so a degree-d hub costs d fewer W values (8 B/edge for
// the common W=float64). The weight argument of [AdjList.AddEdge] and the
// fused-append entry points is accepted but IGNORED, and [AdjList.LoadEntry]
// / [AdjList.LoadEntryH] return a nil weights slice. This is the explicit
// "unweighted graph" representation: every edge is treated as having the
// zero value of W (NOT 1), exactly the all-zero special case the immutable
// CSR snapshot already encodes for a zero-size W (see [graph/csr]).
//
// Weightless is the right choice for a property graph queried only by
// relationships and properties — for example the Cypher engine, which is
// hardwired to W=float64 yet never reads edge weights. It is left
// caller-opt-in (the engine is not auto-defaulted to it) because the weight
// column is load-bearing for weighted algorithms.
//
// Contract for weighted algorithms: a weightless graph models the weight≡0
// special case. Structure-only algorithms (BFS, DFS, connectivity,
// PageRank, label propagation, unweighted betweenness) are unaffected —
// they never read the weight. Weight-consuming algorithms (Dijkstra, A*,
// Bellman-Ford, Johnson, weighted betweenness, MST, max-flow) run on a
// weightless graph as if every edge weight were 0, which yields degenerate
// all-zero distances — this is NOT BFS hop count (that would require every
// weight to be 1). Do not enable Weightless for a graph you intend to query
// with a weighted algorithm.
//
// Weightless is fixed at [New] and never mutated, so [AdjList.Weightless]
// is safe for concurrent use and always returns the same value for the
// lifetime of the AdjList.
Weightless bool
}
Config selects the variant of graph implemented by an AdjList.
The zero value (Directed=false, Multigraph=false) builds a simple undirected graph, which is rarely what users want; prefer constructing a Config explicitly.
type EntryView ¶ added in v0.11.0
type EntryView[W any] struct { // Neighbours is the out-neighbour column. Neighbours []graph.NodeID // Weights is the parallel weight column. Weights []W // Handles is the parallel stable-handle column, or nil when this graph // carries none. Handles []uint64 // Labels is the parallel per-slot relationship-type column, or nil when no // slot of this node has ever been typed. A zero entry means "no type". Labels []uint32 // Aux is the opaque per-slot side column the higher layer attaches — for // lpg, the de-boxed columnar edge properties. Nil when none was attached. Aux AuxColumn }
EntryView is a consistent read of every column of one node's adjacency entry.
All slices alias the immutable entry and MUST NOT be mutated. They are mutually consistent: every non-nil column is the same length as Neighbours, because all of them came from one atomically-published entry.
The zero value is what a node with no outgoing edges reads as.
type Snapshot ¶ added in v0.6.0
type Snapshot[N comparable, W any] struct { // contains filtered or unexported fields }
Snapshot is an immutable, pinned view of an AdjList's adjacency captured at one instant: it holds, per shard, the [shardSlots] version that was current when the snapshot was taken (task #1526, F3.2). Because every adjacency write now publishes a FRESH immutable shardSlots (copy-on-write — see [storeEntry]) rather than mutating the published one in place, the per-shard versions a Snapshot captured stay valid and unchanged for the snapshot's whole lifetime, even while concurrent writers publish newer versions the Snapshot does not see. The Go garbage collector reclaims a retired version once the last Snapshot (and any other reader) holding it is released — the runtime supplies the RCU grace period.
A Snapshot reads adjacency exactly like the live AdjList accessors (Snapshot.LoadEntry, Snapshot.LoadEntryH, Snapshot.LoadEntryLabels, Snapshot.LoadEntryAux, Snapshot.HasEdge) but resolves every slot through its pinned per-shard versions instead of re-loading slotsRef per call, so all reads of one query observe a single, transaction-atomic adjacency state.
Concurrency and the current isolation contract ¶
Snapshot is the in-memory groundwork for a future lock-free adjacency read path (task #1671). In the current stage adjacency reads still run under the higher layer's visibility barrier (lpg.Graph.View / ApplyAtomically), which already gives a barriered reader a stable cross-substructure instant; the pin's per-query stability is therefore redundant with — never weaker than — the barrier today. The Snapshot becomes load-bearing only when reads move out from under the barrier, at which point its pinned immutable versions are what keep an unbarriered reader from observing a partial transaction's adjacency.
The mapper is shared (not copied): NodeIDs are stable for the AdjList's lifetime, so resolving a captured NodeID through the live mapper is sound. Order and Size are intentionally NOT snapshotted here (they read the live mapper / size counter); a Snapshot pins the adjacency topology, which is the F3.2 scope.
Snapshot is safe for concurrent reads from any number of goroutines.
func (*Snapshot[N, W]) HasEdge ¶ added in v0.6.0
HasEdge reports whether the pinned snapshot holds an edge from src to dst, mirroring AdjList.HasEdge. It resolves both endpoints through the shared mapper (NodeIDs are stable for the AdjList's lifetime) and scans the pinned neighbours of src.
func (*Snapshot[N, W]) LoadEntry ¶ added in v0.6.0
LoadEntry returns the pinned neighbours and weights of id, mirroring AdjList.LoadEntry but reading the snapshot's captured per-shard version. The returned slices are owned by the immutable snapshot and must not be mutated. weights is nil for a weightless graph.
func (*Snapshot[N, W]) LoadEntryAux ¶ added in v0.6.0
LoadEntryAux returns the pinned opaque AuxColumn of id, mirroring AdjList.LoadEntryAux over the snapshot's captured version.
func (*Snapshot[N, W]) LoadEntryH ¶ added in v0.6.0
func (s *Snapshot[N, W]) LoadEntryH(id graph.NodeID) (neighbours []graph.NodeID, weights []W, handles []uint64)
LoadEntryH returns the pinned neighbours, weights, and stable handles of id, mirroring AdjList.LoadEntryH over the snapshot's captured version.
func (*Snapshot[N, W]) LoadEntryLabels ¶ added in v0.6.0
LoadEntryLabels returns the pinned per-slot label column of id, mirroring AdjList.LoadEntryLabels over the snapshot's captured version.
type Writer ¶ added in v0.11.0
type Writer[N comparable, W any] struct { // contains filtered or unexported fields }
Writer is an AdjList bound to ONE write transaction: every mutation made through it stamps its version with that transaction's shared commit record and claims a shard's copy-on-write builder under that transaction's identity.
Obtain one with AdjList.Writer. The zero value is unusable; a Writer built from the zero mvcc.Tx is legal and behaves exactly as the AdjList's own methods do — every write is its own transaction, committed the instant it is made.
It is valid only while its transaction's bracket is open and must not be retained past it. A retained Writer does not corrupt anything — a retracted transaction's writes fall back to a fresh untransacted timestamp, see [AdjList.versionStamp] — but it silently stops being transactional, so hold it no longer than the work it was made for.
It carries no state of its own, so it is safe for concurrent use exactly as far as the underlying AdjList and the transaction it names are: two goroutines must not drive ONE write transaction concurrently, and two goroutines with their own transactions may write concurrently.
func (Writer[N, W]) AddEdge ¶ added in v0.11.0
AddEdge is AdjList.AddEdge inside this writer's transaction.
func (Writer[N, W]) AddEdgeH ¶ added in v0.11.0
AddEdgeH is AdjList.AddEdgeH inside this writer's transaction.
func (Writer[N, W]) AddEdgeLabeled ¶ added in v0.11.0
AddEdgeLabeled is AdjList.AddEdgeLabeled inside this writer's transaction.
func (Writer[N, W]) AddEdgeLabeledH ¶ added in v0.11.0
AddEdgeLabeledH is AdjList.AddEdgeLabeledH inside this writer's transaction.
func (Writer[N, W]) AddEdgeLabeledWithProp ¶ added in v0.11.0
AddEdgeLabeledWithProp is AdjList.AddEdgeLabeledWithProp inside this writer's transaction.
func (Writer[N, W]) ClearEdgeLabelSlotValue ¶ added in v0.11.0
ClearEdgeLabelSlotValue is AdjList.ClearEdgeLabelSlotValue inside this writer's transaction.
func (Writer[N, W]) ClearEdgeLabelSlots ¶ added in v0.11.0
ClearEdgeLabelSlots is AdjList.ClearEdgeLabelSlots inside this writer's transaction.
func (Writer[N, W]) ClearEdgeLabelSlotsValue ¶ added in v0.11.0
ClearEdgeLabelSlotsValue is AdjList.ClearEdgeLabelSlotsValue inside this writer's transaction.
func (Writer[N, W]) RemoveAllEdgesFrom ¶ added in v0.11.0
func (wr Writer[N, W]) RemoveAllEdgesFrom(src N)
RemoveAllEdgesFrom is AdjList.RemoveAllEdgesFrom inside this writer's transaction.
func (Writer[N, W]) RemoveEdge ¶ added in v0.11.0
func (wr Writer[N, W]) RemoveEdge(src, dst N)
RemoveEdge is AdjList.RemoveEdge inside this writer's transaction.
func (Writer[N, W]) RemoveEdgeByHandle ¶ added in v0.11.0
RemoveEdgeByHandle is AdjList.RemoveEdgeByHandle inside this writer's transaction.
func (Writer[N, W]) SetEdgeLabelSlot ¶ added in v0.11.0
SetEdgeLabelSlot is AdjList.SetEdgeLabelSlot inside this writer's transaction.
func (Writer[N, W]) SetEdgeLabelSlots ¶ added in v0.11.0
SetEdgeLabelSlots is AdjList.SetEdgeLabelSlots inside this writer's transaction.
func (Writer[N, W]) SetEdgeLabelSlotsAt ¶ added in v0.11.0
SetEdgeLabelSlotsAt is AdjList.SetEdgeLabelSlotsAt inside this writer's transaction.