Documentation
¶
Overview ¶
Package lpg implements the Labelled Property Graph model on top of the github.com/FlavioCFOliveira/GoGraph/graph/adjlist mutable adjacency-list backend.
An LPG decorates each node and each edge with a set of labels (interned strings identifying classes/types) and a bag of typed properties. This package provides labels (see Graph.SetNodeLabel, Graph.SetEdgeLabel) and typed properties (see Graph.SetNodeProperty, Graph.SetEdgeProperty).
Concurrency ¶
The Graph type is safe for concurrent use: every individual operation is internally synchronised — label and property shards by RWMutex, adjacency by lock-free atomic per-shard snapshots, and the per-instance, edge-create-count, and edge-handle stores by mutex — so no single accessor races another.
Transaction-atomic visibility, however, is OPT-IN. A committed transaction may span several operations across several substructures (adjacency, node/edge labels, node/edge properties, tombstones, the roaring label bitmaps, and the secondary indexes).
Isolation comes from an INSTANT, not from a barrier (sprint 334) ¶
Every one of those structures is VERSIONED. A read carries a start timestamp and resolves each structure against it, so it observes exactly the transactions committed at or before that instant — a whole transaction or none of it, and never a torn cross-substructure view. A write publishes its whole transaction with ONE atomic store into a shared commit record, so there is no window in which part of it is visible.
- Per-operation atomicity holds for every accessor, always.
- Partial-transaction-free reads hold for any read carrying a snapshot (Graph.BeginRead / Graph.ReadAt), which is what the Cypher engine and every explicit read transaction take.
- Cross-substructure consistency (e.g. "if the edge exists, both of its endpoint labels exist") holds for the same reads, for the same reason: one instant resolves every structure.
A direct accessor called with NO snapshot reads the present. That is per-operation atomic and is the right answer for a caller outside any transaction, but it is not a transactional view: two such calls can straddle a commit.
Graph.ApplyAtomically still exists and is now the SCHEMA BARRIER — it serialises DDL against readers, not writers against each other. (Graph.View was the read side of that pair; rmp #2344 removed it, and reads now take no barrier at all — they take a snapshot.) An ordinary write holds the barrier SHARED and relies on versioning for its isolation. Reads take no barrier at all.
What this paragraph used to say ¶
It said reads must run inside Graph.View to be partial-transaction-free, and pointed at "the tracked lock-free per-shard snapshot that will make every read transaction-consistent without the barrier" as future work. Neither is true: reads are transaction-consistent WITHOUT the barrier today, and the single-root design that was to deliver it was closed as superseded (rmp #2051, closed by rmp #2311) in favour of the per-object version chains both PostgreSQL and Memgraph use. Recorded rather than silently rewritten, because a reader who remembers the old contract would otherwise look for a lock that nothing takes. See docs/isolation-design.md for the full model.
Index ¶
- func ChainDepthStores() []string
- type EdgeHandleTriple
- type Graph
- func (g *Graph[N, W]) AddEdge(src, dst N, w W) error
- func (g *Graph[N, W]) AddEdgeH(src, dst N, w W) (handle uint64, err error)
- func (g *Graph[N, W]) AddEdgeHIfAbsent(src, dst N, w W, handle uint64) (inserted bool, err error)
- func (g *Graph[N, W]) AddEdgeLabeled(src, dst N, w W, relType string) error
- func (g *Graph[N, W]) AddEdgeLabeledWithProperty(src, dst N, w W, relType, key string, value PropertyValue) error
- func (g *Graph[N, W]) AddEdgeRelTypeOverflowByID(srcID, dstID graph.NodeID, name string) bool
- func (g *Graph[N, W]) AddNode(n N) error
- func (g *Graph[N, W]) AddStoreConstraint(kind uint8, labelName, property string)
- func (g *Graph[N, W]) AddStoreIndex(name string)
- func (g *Graph[N, W]) AdjList() *adjlist.AdjList[N, W]
- func (g *Graph[N, W]) AllocateCommitTS(tx WriteTx) uint64
- func (g *Graph[N, W]) AmbientVersionResolutions() int64
- func (g *Graph[N, W]) AmbientWriteTx() WriteTx
- func (g *Graph[N, W]) AnyEdgeHandlePropertyEverWritten() bool
- func (g *Graph[N, W]) ApplyAtomically(fn func() error) error
- func (g *Graph[N, W]) ApplyAtomicallyTx(fn func(WriteTx) error) error
- func (g *Graph[N, W]) ApplyInVersionedTx(ctx context.Context, tx WriteTx, fn func(WriteTx) error) error
- func (g *Graph[N, W]) ApplyInsideLocked(fn func() error) error
- func (g *Graph[N, W]) ApplyInsideLockedTx(fn func(WriteTx) error) error
- func (g *Graph[N, W]) ApplyVersioned(fn func(WriteTx) error) error
- func (g *Graph[N, W]) ApplyVersionedCtx(ctx context.Context, fn func(WriteTx) error) error
- func (g *Graph[N, W]) AwaitCommitQuiescence(ctx context.Context) error
- func (g *Graph[N, W]) BeginRead() *Snapshot
- func (g *Graph[N, W]) BeginVersionedTx() WriteTx
- func (g *Graph[N, W]) BumpTopoGeneration()
- func (g *Graph[N, W]) ChainDepths() mvcc.Depths
- func (g *Graph[N, W]) ChainDepthsOf(store int) mvcc.Depths
- func (g *Graph[N, W]) ClearStoreConstraints()
- func (g *Graph[N, W]) ClearStoreIndexes()
- func (g *Graph[N, W]) Close() error
- func (g *Graph[N, W]) CloseCtx(ctx context.Context) error
- func (g *Graph[N, W]) Config() adjlist.Config
- func (g *Graph[N, W]) DecEdgeCreateCount(src, dst N)
- func (g *Graph[N, W]) DecrEdgesAdded()
- func (g *Graph[N, W]) DecrEdgesRemoved()
- func (g *Graph[N, W]) DecrNodesAdded()
- func (g *Graph[N, W]) DecrNodesRemoved()
- func (g *Graph[N, W]) DelEdgeProperty(src, dst N, key string)
- func (g *Graph[N, W]) DelEdgePropertyByHandle(src, dst N, handle uint64, key string)
- func (g *Graph[N, W]) DelEdgePropertyByHandleID(srcID, dstID graph.NodeID, handle uint64, key string)
- func (g *Graph[N, W]) DelNodeProperty(n N, key string)
- func (g *Graph[N, W]) EdgeCreateCount(src, dst N) int64
- func (g *Graph[N, W]) EdgeHasProperty(src, dst N, key string) bool
- func (g *Graph[N, W]) EdgeHasPropertyAsOf(src, dst N, key string, snap *Snapshot) bool
- func (g *Graph[N, W]) EdgeIndex() *label.Index
- func (g *Graph[N, W]) EdgeLabels(src, dst N) []string
- func (g *Graph[N, W]) EdgeLabelsAsOf(src, dst N, s *Snapshot) []string
- func (g *Graph[N, W]) EdgeLabelsAt(src, dst N, idx int64) []string
- func (g *Graph[N, W]) EdgeLabelsAtAsOf(src, dst N, idx int64, snap *Snapshot) []string
- func (g *Graph[N, W]) EdgeLabelsByHandle(src, dst N, handle uint64) []string
- func (g *Graph[N, W]) EdgeLabelsByHandleAsOf(src, dst N, handle uint64, snap *Snapshot) []string
- func (g *Graph[N, W]) EdgeLabelsByHandleID(srcID, dstID graph.NodeID, handle uint64) []string
- func (g *Graph[N, W]) EdgeLabelsByHandleIDAsOf(srcID, dstID graph.NodeID, handle uint64, snap *Snapshot) []string
- func (g *Graph[N, W]) EdgeLabelsByID(srcID, dstID graph.NodeID) []string
- func (g *Graph[N, W]) EdgeLabelsByIDAsOf(srcID, dstID graph.NodeID, snap *Snapshot) []string
- func (g *Graph[N, W]) EdgeProperties(src, dst N) map[string]PropertyValue
- func (g *Graph[N, W]) EdgePropertiesAsOf(src, dst N, snap *Snapshot) map[string]PropertyValue
- func (g *Graph[N, W]) EdgePropertiesAt(src, dst N, idx int64) map[string]PropertyValue
- func (g *Graph[N, W]) EdgePropertiesAtAsOf(src, dst N, idx int64, snap *Snapshot) map[string]PropertyValue
- func (g *Graph[N, W]) EdgePropertiesByHandle(src, dst N, handle uint64) map[string]PropertyValue
- func (g *Graph[N, W]) EdgePropertiesByHandleAsOf(src, dst N, handle uint64, snap *Snapshot) map[string]PropertyValue
- func (g *Graph[N, W]) EdgePropertiesByHandleID(srcID, dstID graph.NodeID, handle uint64) map[string]PropertyValue
- func (g *Graph[N, W]) EdgePropertiesByHandleIDAsOf(srcID, dstID graph.NodeID, handle uint64, snap *Snapshot) map[string]PropertyValue
- func (g *Graph[N, W]) EdgePropertiesByID(srcID, dstID graph.NodeID) map[string]PropertyValue
- func (g *Graph[N, W]) EdgePropertiesByIDAsOf(srcID, dstID graph.NodeID, snap *Snapshot) map[string]PropertyValue
- func (g *Graph[N, W]) EdgeSideVersionCount() int64
- func (g *Graph[N, W]) EdgeWeight(src, dst N) (W, bool)
- func (g *Graph[N, W]) EdgeWeightAsOf(src, dst N, snap *Snapshot) (W, bool)
- func (g *Graph[N, W]) EnableLabelDeltas()
- func (g *Graph[N, W]) EnablePropDeltas()
- func (g *Graph[N, W]) EndRead(s *Snapshot)
- func (g *Graph[N, W]) EndVersionedTx(tx WriteTx)
- func (g *Graph[N, W]) EntryViewAsOf(id graph.NodeID, s *Snapshot) adjlist.EntryView[W]
- func (g *Graph[N, W]) FirstEdgeHandle(src, dst N) (uint64, bool)
- func (g *Graph[N, W]) FirstEdgeHandleAsOf(src, dst N, snap *Snapshot) (uint64, bool)
- func (g *Graph[N, W]) ForEachEdgeLabelByID(srcID, dstID graph.NodeID, visit func(name string))
- func (g *Graph[N, W]) ForEachEdgeLabelByIDAsOf(srcID, dstID graph.NodeID, snap *Snapshot, visit func(name string))
- func (g *Graph[N, W]) ForEachEdgeProperty(src, dst N, visit func(name string, pv PropertyValue))
- func (g *Graph[N, W]) ForEachEdgePropertyAsOf(src, dst N, snap *Snapshot, visit func(name string, pv PropertyValue))
- func (g *Graph[N, W]) ForEachEdgePropertyByID(srcID, dstID graph.NodeID, visit func(name string, pv PropertyValue))
- func (g *Graph[N, W]) ForEachEdgePropertyByIDAsOf(srcID, dstID graph.NodeID, snap *Snapshot, ...)
- func (g *Graph[N, W]) ForEachNodeLabelByID(id graph.NodeID, visit func(name string))
- func (g *Graph[N, W]) ForEachNodeLabelByIDAsOf(id graph.NodeID, snap *Snapshot, visit func(name string))
- func (g *Graph[N, W]) ForEachPairOverflowRelTypeByID(srcID, dstID graph.NodeID, visit func(name string))
- func (g *Graph[N, W]) ForEachPairOverflowRelTypeByIDAsOf(srcID, dstID graph.NodeID, s *Snapshot, visit func(name string))
- func (g *Graph[N, W]) ForEachPairSlotRelTypeByID(srcID, dstID graph.NodeID, visit func(ordinal int, name string))
- func (g *Graph[N, W]) ForEachPairSlotRelTypeByIDAsOf(srcID, dstID graph.NodeID, s *Snapshot, visit func(ordinal int, name string))
- func (g *Graph[N, W]) ForEachSlotRelTypeByID(srcID, dstID graph.NodeID, encoded uint32, visit func(name string))
- func (g *Graph[N, W]) ForEachSlotRelTypeByIDAsOf(srcID, dstID graph.NodeID, encoded uint32, snap *Snapshot, ...)
- func (g *Graph[N, W]) GetEdgeProperty(src, dst N, key string) (PropertyValue, bool)
- func (g *Graph[N, W]) GetEdgePropertyAsOf(src, dst N, key string, snap *Snapshot) (PropertyValue, bool)
- func (g *Graph[N, W]) GetNodeProperty(n N, key string) (PropertyValue, bool)
- func (g *Graph[N, W]) GetNodePropertyAsOf(n N, key string, s *Snapshot) (PropertyValue, bool)
- func (g *Graph[N, W]) HasConstraints() bool
- func (g *Graph[N, W]) HasEdgeAsOf(src, dst N, s *Snapshot) bool
- func (g *Graph[N, W]) HasEdgeByIDAsOf(srcID, dstID graph.NodeID, s *Snapshot) bool
- func (g *Graph[N, W]) HasEdgeHandle(src, dst N, handle uint64) bool
- func (g *Graph[N, W]) HasEdgeHandleLabelRecordByID(srcID, dstID graph.NodeID, handle uint64) bool
- func (g *Graph[N, W]) HasEdgeHandleLabelRecordByIDAsOf(srcID, dstID graph.NodeID, handle uint64, snap *Snapshot) bool
- func (g *Graph[N, W]) HasEdgeLabel(src, dst N, name string) bool
- func (g *Graph[N, W]) HasEdgeLabelAsOf(src, dst N, name string, snap *Snapshot) bool
- func (g *Graph[N, W]) HasIndexes() bool
- func (g *Graph[N, W]) HasNodeLabel(n N, name string) bool
- func (g *Graph[N, W]) HasNodeLabelAsOf(n N, name string, s *Snapshot) bool
- func (g *Graph[N, W]) HasNodeLabelByID(id graph.NodeID, name string) bool
- func (g *Graph[N, W]) HasNodeLabelByIDAsOf(id graph.NodeID, name string, s *Snapshot) bool
- func (g *Graph[N, W]) Horizon() *mvcc.Horizon
- func (g *Graph[N, W]) IncEdgeCreateCount(src, dst N) int64
- func (g *Graph[N, W]) IncrEdgesAdded()
- func (g *Graph[N, W]) IncrEdgesRemoved()
- func (g *Graph[N, W]) IncrNodesAdded()
- func (g *Graph[N, W]) IncrNodesRemoved()
- func (g *Graph[N, W]) IndexManager() *index.Manager
- func (g *Graph[N, W]) IndexRemovalBacklog() int64
- func (g *Graph[N, W]) IsTombstoned(id graph.NodeID) bool
- func (g *Graph[N, W]) LabelBitmapAsOf(lid LabelID, s *Snapshot) *roaring64.Bitmap
- func (g *Graph[N, W]) LabelCountBound(lid LabelID, s *Snapshot) (n int64, exact bool)
- func (g *Graph[N, W]) LabelCountExact(lid LabelID, s *Snapshot) (int64, bool)
- func (g *Graph[N, W]) LabelDeltaCount() int64
- func (g *Graph[N, W]) LabelsBitmapAsOf(lids []LabelID, s *Snapshot) *roaring64.Bitmap
- func (g *Graph[N, W]) LabelsCountExact(lids []LabelID, s *Snapshot) (int64, bool)
- func (g *Graph[N, W]) LiveCountExactAsOf(s *Snapshot) bool
- func (g *Graph[N, W]) LiveNodeFilter() func(graph.NodeID) bool
- func (g *Graph[N, W]) LiveOrder() uint64
- func (g *Graph[N, W]) LockBarrier()
- func (g *Graph[N, W]) LockBarrierCtx(ctx context.Context) error
- func (g *Graph[N, W]) MVCCStats() MVCCStats
- func (g *Graph[N, W]) NewSession() *Session[N, W]
- func (g *Graph[N, W]) NextEdgeHandle() uint64
- func (g *Graph[N, W]) NodeExistsAsOf(id graph.NodeID, s *Snapshot) bool
- func (g *Graph[N, W]) NodeIndex() *label.Index
- func (g *Graph[N, W]) NodeInternedAsOf(id graph.NodeID, s *Snapshot) bool
- func (g *Graph[N, W]) NodeLabels(n N) []string
- func (g *Graph[N, W]) NodeLabelsAsOf(n N, s *Snapshot) []string
- func (g *Graph[N, W]) NodeLabelsByID(id graph.NodeID) []string
- func (g *Graph[N, W]) NodeLabelsByIDAsOf(id graph.NodeID, s *Snapshot) []string
- func (g *Graph[N, W]) NodeLabelsInUse() []string
- func (g *Graph[N, W]) NodeLifeVersionCount() int64
- func (g *Graph[N, W]) NodeProperties(n N) map[string]PropertyValue
- func (g *Graph[N, W]) NodePropertiesAsOf(n N, s *Snapshot) map[string]PropertyValue
- func (g *Graph[N, W]) NodePropertiesByID(id graph.NodeID) map[string]PropertyValue
- func (g *Graph[N, W]) NodePropertiesByIDAsOf(id graph.NodeID, s *Snapshot) map[string]PropertyValue
- func (g *Graph[N, W]) NodePropertiesByIDFunc(id graph.NodeID, visit func(name string, pv PropertyValue))
- func (g *Graph[N, W]) NodePropertiesByIDFuncAsOf(id graph.NodeID, snap *Snapshot, visit func(name string, pv PropertyValue))
- func (g *Graph[N, W]) NodePropertyByID(id graph.NodeID, key string) (PropertyValue, bool)
- func (g *Graph[N, W]) NodePropertyByIDAsOf(id graph.NodeID, key string, s *Snapshot) (PropertyValue, bool)
- func (g *Graph[N, W]) OutDegree(src N) (int, bool)
- func (g *Graph[N, W]) OutDegreeBoundedByID(srcID graph.NodeID, limit int) (int, bool)
- func (g *Graph[N, W]) OutDegreeBoundedByIDAsOf(srcID graph.NodeID, limit int, snap *Snapshot) (int, bool)
- func (g *Graph[N, W]) OutDegreeByID(srcID graph.NodeID) (int, bool)
- func (g *Graph[N, W]) OutDegreeByType(src N, relType LabelID) (int, bool)
- func (g *Graph[N, W]) OutDegreeByTypeBounded(src N, relType LabelID, limit int) (int, bool)
- func (g *Graph[N, W]) OutDegreeByTypeBoundedByID(srcID graph.NodeID, relType LabelID, limit int) (int, bool)
- func (g *Graph[N, W]) OutDegreeByTypeBoundedByIDAsOf(srcID graph.NodeID, relType LabelID, limit int, snap *Snapshot) (int, bool)
- func (g *Graph[N, W]) OutDegreeMatchingBoundedByID(srcID graph.NodeID, relType LabelID, typed bool, limit int, ...) (int, bool)
- func (g *Graph[N, W]) OutDegreeMatchingBoundedByIDAsOf(srcID graph.NodeID, relType LabelID, typed bool, limit int, ...) (int, bool)
- func (g *Graph[N, W]) PropDeltaCount() int64
- func (g *Graph[N, W]) PropertyKeys() *PropertyKeyRegistry
- func (g *Graph[N, W]) PropertyKeysInUse() []string
- func (g *Graph[N, W]) ReadAt(snap *Snapshot) *ReadView[N, W]
- func (g *Graph[N, W]) ReclaimNow() int
- func (g *Graph[N, W]) ReclaimVersions(watermark uint64) int
- func (g *Graph[N, W]) Registry() *LabelRegistry
- func (g *Graph[N, W]) RelationshipTypesInUse() []string
- func (g *Graph[N, W]) RemoveAllEdgesFrom(src N)
- func (g *Graph[N, W]) RemoveEdge(src, dst N)
- func (g *Graph[N, W]) RemoveEdgeByHandle(src, dst N, handle uint64) bool
- func (g *Graph[N, W]) RemoveEdgeInstance(src, dst N, idx int64)
- func (g *Graph[N, W]) RemoveEdgeInstanceByHandle(src, dst N, handle uint64)
- func (g *Graph[N, W]) RemoveEdgeLabel(src, dst N, name string)
- func (g *Graph[N, W]) RemoveNode(n N)
- func (g *Graph[N, W]) RemoveNodeLabel(n N, name string)
- func (g *Graph[N, W]) RemoveStoreConstraint(kind uint8, labelName, property string)
- func (g *Graph[N, W]) RemoveStoreIndex(name string)
- func (g *Graph[N, W]) RestoreMVCCClock(floor uint64)
- func (g *Graph[N, W]) RestoreTombstones(ids []graph.NodeID)
- func (g *Graph[N, W]) Revive(n N)
- func (g *Graph[N, W]) SeedEdgeHandle(next uint64)
- func (g *Graph[N, W]) SetConstraintCountSource(src func() int64)
- func (g *Graph[N, W]) SetEdgeLabel(src, dst N, name string)
- func (g *Graph[N, W]) SetEdgeLabelAt(src, dst N, idx int64, name string)
- func (g *Graph[N, W]) SetEdgeLabelByHandle(src, dst N, handle uint64, name string)
- func (g *Graph[N, W]) SetEdgeLabelByHandleID(srcID, dstID graph.NodeID, handle uint64, name string)
- func (g *Graph[N, W]) SetEdgeProperty(src, dst N, key string, value PropertyValue) error
- func (g *Graph[N, W]) SetEdgePropertyAt(src, dst N, idx int64, key string, value PropertyValue) error
- func (g *Graph[N, W]) SetEdgePropertyByHandle(src, dst N, handle uint64, key string, value PropertyValue) error
- func (g *Graph[N, W]) SetEdgePropertyByHandleID(srcID, dstID graph.NodeID, handle uint64, key string, value PropertyValue)
- func (g *Graph[N, W]) SetEdgeRelTypeAtSlotByID(srcID, dstID graph.NodeID, ordinal int, name string) bool
- func (g *Graph[N, W]) SetIndexCountSource(src func() int64)
- func (g *Graph[N, W]) SetIndexManager(m *index.Manager)
- func (g *Graph[N, W]) SetNodeLabel(n N, name string) error
- func (g *Graph[N, W]) SetNodeProperty(n N, key string, value PropertyValue) error
- func (g *Graph[N, W]) SetValidator(v SchemaValidator)
- func (g *Graph[N, W]) SideEffectCounters() (nodesAdded, nodesRemoved, edgesAdded, edgesRemoved uint64)
- func (g *Graph[N, W]) StoreConstraints() []StoreConstraint
- func (g *Graph[N, W]) TombstoneCount() int
- func (g *Graph[N, W]) TombstonedIDs() []graph.NodeID
- func (g *Graph[N, W]) TombstonedIDsAsOf(s *Snapshot) []graph.NodeID
- func (g *Graph[N, W]) TopoGeneration() uint64
- func (g *Graph[N, W]) UnlockBarrier()
- func (g *Graph[N, W]) VacuumStats() VacuumStats
- func (g *Graph[N, W]) ValidateNode(n N) error
- func (g *Graph[N, W]) VersionCount() int64
- func (g *Graph[N, W]) WalkEdgeHandles(fn func(EdgeHandleTriple) bool)
- func (g *Graph[N, W]) WalkEdgeHandlesAsOf(s *Snapshot, fn func(EdgeHandleTriple) bool)
- func (g *Graph[N, W]) Writer(tx WriteTx) WriteView[N, W]
- func (g *Graph[N, W]) WriterView() *ReadView[N, W]
- func (g *Graph[N, W]) WriterViewOf(tx WriteTx) *ReadView[N, W]
- type LabelID
- type LabelRegistry
- type MVCCStats
- type NodeValidator
- type PropertyKeyID
- type PropertyKeyRegistry
- type PropertyKind
- type PropertyValue
- func BoolValue(b bool) PropertyValue
- func BytesValue(b []byte) PropertyValue
- func DateValue(t time.Time) PropertyValue
- func Float64Value(f float64) PropertyValue
- func Int64Value(i int64) PropertyValue
- func ListValue(elems []PropertyValue) PropertyValue
- func StringValue(s string) PropertyValue
- func TimeValue(t time.Time) PropertyValue
- func (p PropertyValue) Bool() (val, ok bool)
- func (p PropertyValue) Bytes() ([]byte, bool)
- func (p PropertyValue) Float64() (float64, bool)
- func (p PropertyValue) Int64() (int64, bool)
- func (p PropertyValue) Kind() PropertyKind
- func (p PropertyValue) List() ([]PropertyValue, bool)
- func (p PropertyValue) String() (string, bool)
- func (p PropertyValue) Time() (time.Time, bool)
- type ReadView
- func (v *ReadView[N, W]) AdjList() *adjlist.AdjList[N, W]
- func (v *ReadView[N, W]) AnyEdgeHandlePropertyEverWritten() bool
- func (v *ReadView[N, W]) At(snap *Snapshot) *ReadView[N, W]
- func (v *ReadView[N, W]) EdgeCreateCount(src, dst N) int64
- func (v *ReadView[N, W]) EdgeHasProperty(src, dst N, key string) bool
- func (v *ReadView[N, W]) EdgeLabels(src, dst N) []string
- func (v *ReadView[N, W]) EdgeLabelsAt(src, dst N, idx int64) []string
- func (v *ReadView[N, W]) EdgeLabelsByHandle(src, dst N, handle uint64) []string
- func (v *ReadView[N, W]) EdgeLabelsByHandleID(srcID, dstID graph.NodeID, handle uint64) []string
- func (v *ReadView[N, W]) EdgeLabelsByID(srcID, dstID graph.NodeID) []string
- func (v *ReadView[N, W]) EdgeProperties(src, dst N) map[string]PropertyValue
- func (v *ReadView[N, W]) EdgePropertiesAt(src, dst N, idx int64) map[string]PropertyValue
- func (v *ReadView[N, W]) EdgePropertiesByHandle(src, dst N, handle uint64) map[string]PropertyValue
- func (v *ReadView[N, W]) EdgePropertiesByHandleID(srcID, dstID graph.NodeID, handle uint64) map[string]PropertyValue
- func (v *ReadView[N, W]) EdgeWeight(src, dst N) (W, bool)
- func (v *ReadView[N, W]) EntryView(id graph.NodeID) adjlist.EntryView[W]
- func (v *ReadView[N, W]) Exists(id graph.NodeID) bool
- func (v *ReadView[N, W]) FirstEdgeHandle(src, dst N) (uint64, bool)
- func (v *ReadView[N, W]) ForEachEdgeLabelByID(srcID, dstID graph.NodeID, visit func(name string))
- func (v *ReadView[N, W]) ForEachEdgeProperty(src, dst N, visit func(name string, pv PropertyValue))
- func (v *ReadView[N, W]) ForEachNodeLabelByID(id graph.NodeID, visit func(name string))
- func (v *ReadView[N, W]) ForEachSlotRelTypeByID(srcID, dstID graph.NodeID, encoded uint32, visit func(name string))
- func (v *ReadView[N, W]) GetEdgeProperty(src, dst N, key string) (PropertyValue, bool)
- func (v *ReadView[N, W]) GetNodeProperty(n N, key string) (PropertyValue, bool)
- func (v *ReadView[N, W]) HasConstraints() bool
- func (v *ReadView[N, W]) HasEdge(src, dst N) bool
- func (v *ReadView[N, W]) HasEdgeByID(srcID, dstID graph.NodeID) bool
- func (v *ReadView[N, W]) HasEdgeHandleLabelRecordByID(srcID, dstID graph.NodeID, handle uint64) bool
- func (v *ReadView[N, W]) HasEdgeLabel(src, dst N, name string) bool
- func (v *ReadView[N, W]) HasNodeLabel(n N, name string) bool
- func (v *ReadView[N, W]) HasNodeLabelByID(id graph.NodeID, name string) bool
- func (v *ReadView[N, W]) IndexManager() *index.Manager
- func (v *ReadView[N, W]) IsTombstoned(id graph.NodeID) bool
- func (v *ReadView[N, W]) LiveNodeCountExact() (uint64, bool)
- func (v *ReadView[N, W]) LiveNodeFilter() func(graph.NodeID) bool
- func (v *ReadView[N, W]) LiveOrder() uint64
- func (v *ReadView[N, W]) NodeIndex() *label.Index
- func (v *ReadView[N, W]) NodeLabels(n N) []string
- func (v *ReadView[N, W]) NodeLabelsByID(id graph.NodeID) []string
- func (v *ReadView[N, W]) NodeProperties(n N) map[string]PropertyValue
- func (v *ReadView[N, W]) NodePropertiesByIDFunc(id graph.NodeID, visit func(name string, pv PropertyValue))
- func (v *ReadView[N, W]) NodePropertyByID(id graph.NodeID, key string) (PropertyValue, bool)
- func (v *ReadView[N, W]) OutDegreeBoundedByID(id graph.NodeID, limit int) (int, bool)
- func (v *ReadView[N, W]) OutDegreeByTypeBoundedByID(id graph.NodeID, relType LabelID, limit int) (int, bool)
- func (v *ReadView[N, W]) OutDegreeMatchingBoundedByID(id graph.NodeID, relType LabelID, typed bool, limit int, ...) (int, bool)
- func (v *ReadView[N, W]) PropertyKeys() *PropertyKeyRegistry
- func (v *ReadView[N, W]) Raw() *Graph[N, W]
- func (v *ReadView[N, W]) Registry() *LabelRegistry
- func (v *ReadView[N, W]) Snapshot() *Snapshot
- func (v *ReadView[N, W]) StoreConstraints() []StoreConstraint
- func (v *ReadView[N, W]) TopoGeneration() uint64
- type SchemaValidator
- type Session
- func (s *Session[N, W]) ApplyVersioned(fn func(WriteTx) error) error
- func (s *Session[N, W]) ApplyVersionedCtx(ctx context.Context, fn func(WriteTx) error) error
- func (s *Session[N, W]) Await(ctx context.Context) error
- func (s *Session[N, W]) BeginRead() *Snapshot
- func (s *Session[N, W]) BeginReadCtx(ctx context.Context) (*Snapshot, error)
- func (s *Session[N, W]) BeginVersionedTx() (WriteTx, error)
- func (s *Session[N, W]) BeginVersionedTxCtx(ctx context.Context) (WriteTx, error)
- func (s *Session[N, W]) EndVersionedTx(tx WriteTx)
- func (s *Session[N, W]) Floor() uint64
- func (s *Session[N, W]) Graph() *Graph[N, W]
- type Snapshot
- type StoreConstraint
- type VacuumStats
- type WriteTx
- type WriteView
- func (wv WriteView[N, W]) AddEdge(src, dst N, w W) error
- func (wv WriteView[N, W]) AddEdgeH(src, dst N, w W) (uint64, error)
- func (wv WriteView[N, W]) AddEdgeHIfAbsent(src, dst N, w W, handle uint64) (bool, error)
- func (wv WriteView[N, W]) AddNode(n N) error
- func (wv WriteView[N, W]) DelEdgeProperty(src, dst N, key string)
- func (wv WriteView[N, W]) DelEdgePropertyByHandle(src, dst N, handle uint64, key string)
- func (wv WriteView[N, W]) DelNodeProperty(n N, key string)
- func (wv WriteView[N, W]) Graph() *Graph[N, W]
- func (wv WriteView[N, W]) NoteConstraintTouch(n N) error
- func (wv WriteView[N, W]) Read() *ReadView[N, W]
- func (wv WriteView[N, W]) RemoveAllEdgesFrom(src N)
- func (wv WriteView[N, W]) RemoveEdge(src, dst N)
- func (wv WriteView[N, W]) RemoveEdgeByHandle(src, dst N, handle uint64) bool
- func (wv WriteView[N, W]) RemoveEdgeInstance(src, dst N, idx int64)
- func (wv WriteView[N, W]) RemoveEdgeInstanceByHandle(src, dst N, handle uint64)
- func (wv WriteView[N, W]) RemoveEdgeLabel(src, dst N, name string)
- func (wv WriteView[N, W]) RemoveNode(n N)
- func (wv WriteView[N, W]) RemoveNodeLabel(n N, name string)
- func (wv WriteView[N, W]) Revive(n N)
- func (wv WriteView[N, W]) SetEdgeLabel(src, dst N, name string)
- func (wv WriteView[N, W]) SetEdgeLabelAt(src, dst N, idx int64, name string)
- func (wv WriteView[N, W]) SetEdgeLabelByHandle(src, dst N, handle uint64, name string)
- func (wv WriteView[N, W]) SetEdgeProperty(src, dst N, key string, value PropertyValue) error
- func (wv WriteView[N, W]) SetEdgePropertyAt(src, dst N, idx int64, key string, value PropertyValue) error
- func (wv WriteView[N, W]) SetEdgePropertyByHandle(src, dst N, handle uint64, key string, value PropertyValue) error
- func (wv WriteView[N, W]) SetNodeLabel(n N, name string) error
- func (wv WriteView[N, W]) SetNodeProperty(n N, key string, value PropertyValue) error
- func (wv WriteView[N, W]) Tx() WriteTx
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ChainDepthStores ¶ added in v0.11.0
func ChainDepthStores() []string
ChainDepthStores returns the metric-name suffix of each store that reports a distribution, so a caller can label Graph.ChainDepthsOf without knowing the enumeration.
Types ¶
type EdgeHandleTriple ¶
EdgeHandleTriple is one live durable edge identity: the (src, dst) endpoint NodeIDs and the stable handle stamped on that slot. Emitted by Graph.WalkEdgeHandles for the snapshot writer.
type Graph ¶
type Graph[N comparable, W any] struct { // contains filtered or unexported fields }
Graph is a labelled property graph generic over the user node type N and edge weight type W. It composes an adjlist.AdjList with a label registry and per-vertex / per-edge label storage backed by label.Index bitmaps.
Example ¶
ExampleGraph builds a small labelled property graph: nodes carry labels (their classes) and typed properties, and edges connect them. The Config is forwarded to the underlying adjacency list, so Directed selects a directed graph here.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)
func main() {
g := lpg.New[string, int](adjlist.Config{Directed: true})
// Create two nodes and tag each with a label.
_ = g.AddNode("alice")
_ = g.AddNode("bob")
_ = g.SetNodeLabel("alice", "Person")
_ = g.SetNodeLabel("bob", "Person")
// Attach typed properties via the PropertyValue constructors.
_ = g.SetNodeProperty("alice", "name", lpg.StringValue("Alice"))
_ = g.SetNodeProperty("alice", "age", lpg.Int64Value(30))
// Connect them with a labelled edge.
_ = g.AddEdge("alice", "bob", 0)
g.SetEdgeLabel("alice", "bob", "KNOWS")
name, _ := g.GetNodeProperty("alice", "name")
nameStr, _ := name.String()
age, _ := g.GetNodeProperty("alice", "age")
ageInt, _ := age.Int64()
fmt.Println("alice is Person:", g.HasNodeLabel("alice", "Person"))
fmt.Println("alice.name:", nameStr)
fmt.Println("alice.age:", ageInt)
fmt.Println("alice KNOWS bob:", g.HasEdgeLabel("alice", "bob", "KNOWS"))
}
Output: alice is Person: true alice.name: Alice alice.age: 30 alice KNOWS bob: true
func New ¶
func New[N comparable, W any](cfg adjlist.Config) *Graph[N, W]
New returns a fresh LPG built on top of a new adjlist.AdjList configured by cfg.
func (*Graph[N, W]) AddEdge ¶
AddEdge inserts a directed edge (mirrored when the graph is undirected) from src to dst with weight w. The error contract matches the underlying adjlist.AdjList.AddEdge: callers must propagate adjlist.ErrShardFull when the responsible shard is at adjlist.Config.MaxShardCapacity.
AddEdge does NOT revive a tombstoned endpoint: only Graph.AddNode clears a tombstone. The contract is that callers materialise node patterns via AddNode before linking them, so a live edge is never created onto a logically-removed node. The query executor upholds this (CREATE routes every endpoint through the mutator's AddNode).
func (*Graph[N, W]) AddEdgeH ¶
AddEdgeH inserts a directed edge exactly like Graph.AddEdge but first allocates a stable per-edge handle for it and stamps that handle onto the adjacency slot (via adjlist.AdjList.AddEdgeH). It returns the handle so the caller can key per-instance edge metadata (SetEdgeLabelByHandle / SetEdgePropertyByHandle) by an identity that survives sibling-edge deletion, instead of the positional CREATE index that the old read path re-derived from CSR slot order.
The returned handle is always non-zero. On the simple-graph collapse of a duplicate (src, dst) the underlying adjacency no-ops the slot write and the supplied handle is not stored, but a fresh handle value is still consumed (monotonicity is a property of the counter, not of storage), so callers must treat the handle as advisory in simple-graph mode and keep using the per-pair / per-CREATE-index surfaces there. See edge_handle.go.
AddEdgeH honours the same error and revival contract as Graph.AddEdge.
func (*Graph[N, W]) AddEdgeHIfAbsent ¶
AddEdgeHIfAbsent inserts a directed edge (src, dst, w) stamped with the explicit stable `handle`, but only when no edge with that handle already exists on the (src, dst) pair (Graph.HasEdgeHandle). When the handle is already present the call is a no-op and returns (false, nil): the edge was loaded by the snapshot or applied by an earlier WAL frame, so re-inserting it would create a spurious parallel duplicate. When the handle is absent the edge is inserted via the explicit-handle adjacency path (adjlist.AdjList.AddEdgeH) and the call returns (true, nil).
AddEdgeHIfAbsent is the replay primitive that makes snapshot + full-WAL recovery idempotent without a second live-handle index. It does NOT advance the handle counter — the handle is supplied by the durable record, not freshly minted; Graph.SeedEdgeHandle re-seeds the counter once after replay.
A handle of 0 is treated as "no durable identity" and falls back to a plain Graph.AddEdge so a pre-Stage-2 WAL frame (which carried no handle) still replays. AddEdgeHIfAbsent is NOT safe for concurrent use.
func (*Graph[N, W]) AddEdgeLabeled ¶ added in v0.6.0
AddEdgeLabeled inserts a directed edge (mirrored when the graph is undirected) from src to dst with weight w and tags it with the relationship-type name in a SINGLE adjacency operation: the type is interned and written into the edge's inline label slot AT insertion time, instead of the two-step Graph.AddEdge + Graph.SetEdgeLabel which copies the whole label column after the append. For a bulk labelled build this restores O(degree) amortised cost per source (the fused append is O(1) amortised), versus the O(degree²) a per-edge column copy-on-write would cost.
AddEdgeLabeled is the labelled-build fast path. For the simple single-label case its observable result is identical to AddEdge followed by SetEdgeLabel: the type lands in the first dst-matching inline slot, so Graph.EdgeLabels, Graph.HasEdgeLabel, the per-slot label scan, and the TCK read path all see exactly the same derived label set. To ADD A SECOND distinct type to an already-labelled pair, or to (re)label a PRE-EXISTING edge, use Graph.SetEdgeLabel; that path keeps its general copy-on-write semantics and the overflow spill for multi-label pairs.
The coarse src-keyed edge-label index (g.edgeIdx) is updated exactly as SetEdgeLabel updates it, so index-driven candidate enumeration is unaffected.
AddEdgeLabeled honours the same error and revival contract as Graph.AddEdge: it propagates adjlist.ErrShardFull and does NOT revive a tombstoned endpoint. When the underlying adjacency no-ops the insertion (a simple-graph duplicate (src, dst)) the supplied type is not stamped on the existing slot; callers that may re-label an existing edge must use SetEdgeLabel.
AddEdgeLabeled is safe for concurrent use.
func (*Graph[N, W]) AddEdgeLabeledWithProperty ¶ added in v0.6.0
func (g *Graph[N, W]) AddEdgeLabeledWithProperty(src, dst N, w W, relType, key string, value PropertyValue) error
AddEdgeLabeledWithProperty inserts a directed edge (mirrored when the graph is undirected) from src to dst with weight w, tags it with the relationship-type name, AND records one property (key, value) on it — all in a SINGLE adjacency operation. Both the type and the property value are written into the new edge's inline slot AT insertion time, instead of the three-step Graph.AddEdgeLabeled + Graph.SetEdgeProperty whose final step copies the whole per-source property column. For a bulk property-carrying build this restores O(degree) amortised cost per source (the fused append is O(1) amortised), versus the O(degree²) the per-edge column copy-on-write of Graph.SetEdgeProperty costs.
AddEdgeLabeledWithProperty is the property-carrying labelled-build fast path. Its observable result is identical to AddEdgeLabeled followed by SetEdgeProperty for the simple single-edge-per-pair case the bulk builders use: the type lands in the first dst-matching inline slot and the value lands on the new slot's columnar block, so Graph.EdgeProperties, Graph.GetEdgeProperty, the per-pair coalesce, and the TCK read path all see exactly the same derived state. To set a SECOND property on the edge, or to mutate a PRE-EXISTING edge, use Graph.SetEdgeProperty; that path keeps its general copy-on-write semantics.
If the installed SchemaValidator rejects the value the edge is NOT inserted and the error is returned (validation runs before any mutation), so the fused write keeps the same all-or-nothing contract as a validated SetEdgeProperty. AddEdgeLabeledWithProperty otherwise honours the same error and revival contract as Graph.AddEdge: it propagates adjlist.ErrShardFull and does NOT revive a tombstoned endpoint. When the underlying adjacency no-ops the insertion (a simple-graph duplicate (src, dst)) neither the type nor the property is stamped on the existing slot.
A date-shaped string value (a Cypher Date delivered as a SOH-tagged canonical string) is folded into the int32 epoch-day column exactly as SetEdgeProperty folds it, so it round-trips to a native Date through the Cypher read path.
AddEdgeLabeledWithProperty is safe for concurrent use.
func (*Graph[N, W]) AddEdgeRelTypeOverflowByID ¶ added in v0.11.0
AddEdgeRelTypeOverflowByID adds name to the OVERFLOW list of the directed pair (srcID → dstID) — the per-pair half of its type state, read by [Graph.slotCarriesType] as carried by every column-typed slot of the pair. It reports whether the list changed; re-asserting a type already present reports false.
It exists so the snapshot apply path can restore a pair's overflow exactly as it stood, instead of re-deriving it from a placement heuristic. Ordinary callers should use Graph.SetEdgeLabel, which decides for itself whether a type fits in a slot's column or has to spill.
Unlike Graph.SetEdgeLabel it does NOT require the edge to exist: an overflow entry for an absent pair is inert, and the apply path has already checked adjlist.AdjList.HasEdge before calling.
AddEdgeRelTypeOverflowByID is safe for concurrent use.
func (*Graph[N, W]) AddNode ¶
AddNode inserts n if not already present. The error contract matches the underlying adjlist.AdjList.AddNode: callers must propagate adjlist.ErrShardFull when the responsible shard is at adjlist.Config.MaxShardCapacity.
AddNode also clears any tombstone on n: re-creating a node that was previously removed via Graph.RemoveNode brings it back to life under the same stable NodeID (resurrection). This is the single node- materialising entry point through which a delete→recreate cycle flows — in-process, on WAL replay, and on snapshot apply — so it is the one place that must revive. Graph.SetNodeLabel does not revive: a tombstoned node is never matched by a read clause, so a label can only reach a removed key after AddNode has already revived it.
func (*Graph[N, W]) AddStoreConstraint ¶ added in v0.6.0
AddStoreConstraint records that a schema constraint of the given kind on (label, property) is declared through the txn.Store-direct API. It is the store-layer dual of the cypher engine's syncConstraintCount: the txn.Store commit-apply path calls it for every committed OpCreateConstraint so that Graph.HasConstraints reports the constraint to a WAL-truncating checkpoint, independent of whether a cypher engine is wired in (#1756).
The (kind, label, property) key makes re-declaring the same constraint idempotent — the active count never over-counts a single durable constraint, the only direction that could let a checkpoint silently drop it.
AddStoreConstraint is safe for concurrent use.
func (*Graph[N, W]) AddStoreIndex ¶ added in v0.6.0
AddStoreIndex records that a secondary index named name is declared through the txn.Store-direct API. It is the store-layer dual of the cypher engine's index-def registry: the txn.Store commit-apply path calls it for every committed OpCreateIndex so that Graph.HasIndexes reports the index to a WAL-truncating checkpoint, independent of whether a cypher engine is wired in (#1755).
The index NAME key makes re-declaring the same index idempotent — the active count never over-counts a single durable index, the only direction that could let a checkpoint silently drop it.
AddStoreIndex is safe for concurrent use.
func (*Graph[N, W]) AllocateCommitTS ¶ added in v0.11.0
AllocateCommitTS reserves this transaction's commit timestamp WITHOUT making it visible, and returns it. It is idempotent: a second call returns the same value.
It returns zero — meaning "no timestamp" — for the zero transaction and for a graph whose versioning substrate is disarmed, so a durable caller may invoke it unconditionally and encode whatever it gets.
What it is for (rmp #2309) ¶
A durable writer must put the commit instant INTO the WAL record, because the MVCC clock is restored at recovery by deriving it from the WAL rather than by trusting a persisted counter. That is impossible if the instant is minted after the record is written, which is what [Graph.endWrite] used to do — it runs from the caller's deferred teardown, strictly after the append and the fsync.
So the sequence becomes:
AllocateCommitTS → encode the OpCommit marker → fsync → EndVersionedTx (publish)
which is PostgreSQL's ordering: the XID is assigned before XLogFlush, the flushed record carries it, and only then is the commit marked visible.
The caller's obligation, and why it is discharged elsewhere ¶
An allocated timestamp MUST eventually be published or abandoned. One that is neither stalls the contiguous commit frontier permanently — every later commit becomes invisible to new readers, and the commit log grows without bound.
The caller does NOT discharge it directly. [Graph.endWrite] does, on every path: it publishes on success and abandons on abort and on the versioned-nothing case. That is deliberate — a discharge placed beside each caller is one a new caller can forget, and the failure mode is a silent, permanent stall rather than a crash. So the only obligation here is the one that already existed: call Graph.EndVersionedTx exactly once per transaction.
It lengthens the in-flight window, on purpose ¶
Between this call and the publish sits a WAL fsync, so a transaction now holds an unpublished timestamp for milliseconds rather than nanoseconds, and ONE in-flight commit holds the frontier back for every reader. That cost is real and it is observable: MVCCStats.InFlightCommits is the measure.
Safe for concurrent use; each goroutine must pass its own transaction.
func (*Graph[N, W]) AmbientVersionResolutions ¶ added in v0.11.0
AmbientVersionResolutions returns how many versions have resolved their transaction through the graph's AMBIENT slot rather than carrying it — the resolution rmp #2320 removed from the Cypher and store write paths.
It is the observable form of an invariant that is otherwise only assertable by inspecting version chains: a write path that carries its transaction leaves this counter untouched, and a single ambient resolution inside a statement is enough to split that statement across two commit records once a second write bracket is open. Sample it before and after a region and require the difference to be zero.
A non-zero difference is not automatically a defect: the direct Go-API mutators resolve this way BY CONTRACT — they are per-operation atomic, not transactional — as do the bulk builders, WAL replay and snapshot apply. It is a defect for any path that runs inside a write bracket.
Cumulative and never reset, so two observers cannot take it from each other.
Safe for concurrent use.
func (*Graph[N, W]) AmbientWriteTx ¶ added in v0.11.0
AmbientWriteTx returns the write transaction the graph's slot currently names, for a caller that holds the barrier EXCLUSIVELY and is therefore the only open bracket — the explicit-transaction path, which opens its transaction in Graph.LockBarrier and runs its statements through Graph.ApplyInsideLocked later, with no closure to carry the handle in.
Any other caller must use the handle Graph.ApplyVersioned gave it. This one is correct by virtue of the exclusive hold and by nothing else.
func (*Graph[N, W]) AnyEdgeHandlePropertyEverWritten ¶ added in v0.11.0
AnyEdgeHandlePropertyEverWritten reports whether a by-handle edge property has EVER been written to this graph. It is a one-way latch, not a live count: it is set before the first such write becomes visible and is never cleared, so a later delete, transaction abort or vacuum leaves it true even though the store is empty again.
It answers exactly one question — "can Graph.EdgePropertiesByHandle and its variants possibly return anything?" — so a caller whose only use for the by-handle map is to discover that it is empty can skip the read entirely. A false result is a proof of absence; a true result is not a proof of presence, so it must never be used to report that a property EXISTS. Read it with that asymmetry in mind: false is exact, true is conservative.
The intended use is a probe skip on a graph populated through the Go API (Graph.AddEdgeH plus Graph.SetEdgeProperty), which stamps a handle but records properties in the per-pair store only, so the by-handle store stays empty for the process lifetime (rmp #2387).
AnyEdgeHandlePropertyEverWritten is safe for concurrent use and takes no lock.
func (*Graph[N, W]) ApplyAtomically ¶
ApplyAtomically runs fn while holding the graph's transaction-visibility write lock, which excludes every other WRITER for the duration of fn. fn is the in-memory apply of one durable transaction; callers invoke it only after the transaction's WAL frames are fsynced.
By itself it does NOT make fn's writes atomically visible; one threaded transaction does ¶
This paragraph used to promise that every mutation fn performs "becomes visible to Graph.View readers as a single atomic step". That guarantee was scoped to a reader type that **no longer exists**: Graph.View was removed by rmp #2344, and snapshots (Graph.BeginRead / Graph.ReadAt) are now the only readers. The promise was never restated for them, and it does not carry over.
The reason is [Graph.deltaStamp]: a write that passes a NIL transaction record — which is what the bare exported mutators such as Graph.AddEdge and Graph.SetNodeLabel do — takes a FRESH commit instant of its own. Several such writes inside one ApplyAtomically bracket therefore commit at several distinct instants, and a snapshot whose startTS lands between two of them observes a PARTIAL set. Measured under a full `go test -race ./...` peer load: an edge plus two labels written this way tore in 5 runs out of 40, with the reader seeing the edge and neither label (rmp #2378).
SO THREAD ONE TRANSACTION. Use Graph.ApplyAtomicallyTx and issue the writes through Graph.Writer, so deltaStamp answers every write with the same record and they share one commit instant. The same requirement is stated on Graph.ApplyInsideLockedTx.
THREADING ONE TRANSACTION WAS NECESSARY BUT, FOR A TIME, NOT SUFFICIENT — AND THAT GAP IS NOW CLOSED. Removing the three-separate-instants cause left a residual tear: the same workload still tore in 4 runs of 100, in both pairings and both directions. rmp #2378 found why and fixed it in commit 509929e2: mvcc.Visible was re-evaluated per substructure against a MUTABLE CommitInfo.ts that flips at commit, so a reader straddling a commit split. A Snapshot now PINS the visibility verdict per commit record, and AdjList resolves through it instead of short-circuiting on the global versionActive counter. Both halves were required — the pinned verdict alone still tore 2 in 100 and the counter alone 3 in 100 — and together they measured ZERO IN 300 RUNS against a pre-fix rate of 2 to 5 per 100, under the peer load that was the only environment able to reproduce it. Read-path cost and allocations were unchanged.
So a caller threading ONE transaction may now rely on writes across different substructures, and across different label shards, becoming visible together. A caller that does NOT thread one transaction still cannot: the several distinct commit instants described above are a separate cause and are not affected by that fix.
Exclusive-writer brackets around state that is not versioned per-write — an index registration, for example — are unaffected, since there is no commit instant to split.
ApplyAtomically must not be called re-entrantly, and the mutations inside fn must not call Graph.ApplyAtomically (the RWMutex is not re-entrant, so a nested acquisition from this goroutine would deadlock).
The invariant is CHECKED in builds made with -race or -tags gograph_debug: a nested call from a goroutine already inside the barrier panics with a clear message instead of deadlocking. The panic indicates a programmer error and is not recovered by this package. A released build omits the check, because identifying the calling goroutine costs a runtime.Stack call that measured 97-99% of this method and did not scale with cores; there, violating the invariant deadlocks silently. Build with -tags gograph_debug to diagnose a suspected freeze. See graph/lpg/reentrancy_disabled.go for the full rationale.
The graph's per-shard write methods that fn calls take their own shard locks beneath visMu, which is safe because visMu is acquired only here and in View.
Concurrent calls from DIFFERENT goroutines are unaffected: they serialise on visMu as before, and the guard never trips on them.
It IS the bulk-load bracket (rmp #2395) ¶
Beyond excluding other writers, this method opens a write TRANSACTION for the duration of fn, and that is what makes it the bulk-load bracket. The adjacency's clone-once dedup keys on a non-zero BUILDER OWNER ([adjlist.AdjList.storeEntry] takes it from the write's own transaction, else from [adjlist.AdjList.builderOwner], which prefers the ambient transaction's id and falls back to the token adjlist.AdjList.BeginCommit mints). With an owner, each touched shard's slot array is cloned AT MOST ONCE and then mutated in place. With NO owner — a direct Go-API write outside any bracket — every single write clones the whole array again. A bulk load that appends edges one call at a time therefore pays a copy per edge, which the 2026-08-10 profile sweep measured at 50.75% of every object allocated by examples/01_basic.
So the ownership matters, not the window as such: this bracket also calls BeginCommit, but that call is redundant here and the transaction is what carries the dedup. BeginCommit exists for the exclusive paths that write with NO transaction open — single-threaded WAL replay and bulk import — which is why it documents a narrower single-writer contract.
So a caller loading many edges should wrap the loop rather than reach for a different API — there is no separate bulk-import entry point on Graph, and none is needed. Measured on a 5 000-node / 101 974-edge build, three interleaved rounds in one process, with the resulting graph verified byte-identical by an order-sensitive fingerprint over every out-neighbour and weight:
per-edge, unbracketed 86 MB 1.36M objects 69 ms one ApplyAtomically 58 MB 1.06M objects 50 ms (0.68x / 0.78x) ApplyAtomically per 10k 60 MB 1.11M objects 55 ms (0.70x / 0.81x)
The bracket changes COST, never CONTENT. Chunking recovers almost all of the win while bounding how long the exclusive barrier is held, which is the shape to prefer when the load cannot lock out readers for its whole duration — a single bracket over a very large load blocks every other writer, and every snapshot-taking reader, until fn returns.
TWO mechanisms produce that saving, and they were separated by measurement rather than assumed: forcing the adjacency's dedup off leaves the bracketed arm at 0.921x instead of 0.758x, so roughly two thirds of the objects saved are the per-edge slot-array clone and the remaining third is ONE shared MVCC commit record in place of a fresh one per write. Both follow from the transaction, which is why bracketing is the whole answer and no separate bulk-import API is needed. graph/lpg/bulkload_bracket_test.go pins the combined effect against a threshold chosen between those two regimes, so losing the dedup alone fails the test.
Two cautions:
- This buys ALLOCATION, not atomic visibility. Everything the sections above say about writes committing at several distinct instants still applies; use Graph.ApplyAtomicallyTx with Graph.Writer when the load must also land at one instant. ApplyAtomicallyTx opens the same window (both go through openWriteBracket), so it costs nothing to prefer it.
- A caller writing directly against adjlist.AdjList rather than Graph — as examples/01_basic does — has no transaction to borrow an owner from and brackets with adjlist.AdjList.BeginCommit/EndCommit instead, honouring that pair's narrower single-writer contract itself. Graph's bracket cannot leak, because it closes on every path out of fn including a panic; the raw pair can, which is why it spells that contract out.
The equivalent bracket is why store/recovery's snapshot apply records 737.6 MiB -> 113.6 MiB and 147.45 ms -> 73.57 ms at 50k nodes / 500k edges (rmp #2170) and why WAL replay brackets itself (rmp #1526).
func (*Graph[N, W]) ApplyAtomicallyTx ¶ added in v0.11.0
ApplyAtomicallyTx is Graph.ApplyAtomically for a caller that needs the transaction handle its bracket opened — the same exclusive bracket, with the handle passed in rather than left to be looked up.
It exists so the Cypher engine can thread one shape of apply function over both the exclusive and the shared bracket (Graph.ApplyVersioned) and over Graph.ApplyInsideLockedTx, instead of resolving the writer's transaction off the graph in three different places.
func (*Graph[N, W]) ApplyInVersionedTx ¶ added in v0.11.0
func (g *Graph[N, W]) ApplyInVersionedTx(ctx context.Context, tx WriteTx, fn func(WriteTx) error) error
ApplyInVersionedTx runs fn AS tx, holding the schema barrier SHARED for the duration of fn and nothing longer. It is the per-statement bracket of a multi-statement transaction opened with Graph.BeginVersionedTx.
The shared hold is what a statement genuinely needs: a catalog — the declared indexes and constraints, and the structures a DDL transition rebuilds — that does not change underneath it. It does not exclude another writer, and it must not: concurrent statements from different transactions overlap, and a collision between them is arbitrated by the version chain, not by this lock.
It differs from Graph.ApplyVersioned in exactly one way, and it is the important one: ApplyVersioned opens and closes a transaction around fn, so each call is its own atomic unit, whereas this runs fn inside a transaction the caller already owns. Nothing is published when fn returns; publication happens once, in Graph.EndVersionedTx.
It also differs from Graph.ApplyInsideLockedTx, which resolves the transaction from the graph's AMBIENT slot and is therefore only correct while a caller holds the barrier exclusively. This takes the transaction as a parameter for the reason rmp #2320 established: with concurrent writers the ambient slot names whichever transaction published last.
The acquisition is bounded by ctx, so a caller with a deadline is not held by a concurrent DDL for longer than it agreed to wait (rmp #2174). When ctx finishes first, fn does NOT run, nothing is held, and ctx's error is returned — the caller's transaction remains open and usable.
fn must not call Graph.ApplyAtomically or Graph.ApplyVersioned: the hold is shared, and Go's sync.RWMutex prefers a queued writer, so a nested shared acquisition deadlocks the instant one queues. Enforced by the re-entrancy guard under -race or -tags gograph_debug.
Safe for concurrent use; each goroutine must pass its own transaction.
func (*Graph[N, W]) ApplyInsideLocked ¶ added in v0.3.0
ApplyInsideLocked is the barrier-already-held variant of Graph.ApplyAtomically. It runs fn directly without acquiring or releasing visMu — the caller MUST already hold the barrier via Graph.LockBarrier. The re-entrancy guard is NOT re-checked (the caller's stamp stays in effect) and the lock is NOT released afterward.
This method exists solely to satisfy callers that hold the barrier for the lifetime of an explicit transaction (task #1412) and need to run a sub-operation (e.g. one Exec statement) under the same already-held lock. Calling this method without first calling LockBarrier yields undefined behaviour.
func (*Graph[N, W]) ApplyInsideLockedTx ¶ added in v0.11.0
ApplyInsideLockedTx is Graph.ApplyInsideLocked with the enclosing transaction's handle. It opens NO transaction of its own — the statement must share the record the enclosing Graph.LockBarrier opened, or the explicit transaction is not atomically visible — and takes the handle off the graph's slot, which the exclusive hold makes unambiguous (see Graph.AmbientWriteTx).
Calling it without holding the barrier via LockBarrier yields undefined behaviour, exactly as with ApplyInsideLocked.
func (*Graph[N, W]) ApplyVersioned ¶ added in v0.11.0
ApplyVersioned runs fn as one write transaction WITHOUT excluding other writers: it holds the schema barrier in SHARED mode, so concurrent ApplyVersioned brackets overlap and are serialised only by the per-object latches that guard each version-chain head (rmp #2304).
This is the ordinary write path — the Cypher engine's autocommit statement and the durable store's in-memory apply. Graph.ApplyAtomically remains the EXCLUSIVE bracket, and what is left inside it is catalog work: index and constraint registration, and the checkpointer's capture. See the visMu field comment for the division and for the prior art it follows.
What delivers atomic visibility now that a lock does not ¶
The guarantee is unchanged and its mechanism is different. Every version fn creates points at ONE commit record, and [Graph.endWrite] publishes that record's commit timestamp with a single atomic store, so a concurrent reader resolving through mvcc.Visible observes either every version of the transaction or none of them — however many stores they span, and whether or not any other writer is mid-apply. Exclusion made the same promise by making the interleaving impossible; versioning makes it by making the interleaving unobservable. That substitution is only sound because A1-A5 and B1 landed first: out-of-order commit publication (rmp #2298), a writer snapshot with a real transaction id (#2299), per-object write-write conflict detection (#2300), per-transaction commit state (#2301), WAL frame contiguity (#2302) and a publication order for the derived structures (#2303).
What the shared hold is still for ¶
Not writers — DDL. A schema change must see a graph in which no write is half-applied, and it has no snapshot to read that from because the catalog it mutates is not versioned. Holding this shared for the whole bracket, including the transaction's publication, is what lets Graph.ApplyAtomically wait for every in-flight write to become visible before it registers an index or validates a constraint. Memgraph draws the line in the same place — an ordinary write takes `main_lock_` with a `std::shared_lock` and only the index/constraint and durability transitions take it uniquely (memgraph/memgraph, branch master, read 2026-08-02; src/storage/v2/inmemory/storage.cpp) — and PostgreSQL expresses it through the conflict matrix, where an ordinary write's RowExclusiveLock does not conflict with itself and CREATE INDEX's ShareLock does (src/backend/storage/lmgr/lock.c, LockConflicts).
fn must not call Graph.ApplyAtomically or ApplyVersioned: the hold is shared, and Go's sync.RWMutex prefers a queued writer, so a nested shared acquisition deadlocks the instant one queues. Enforced by the same re-entrancy guard, under -race or -tags gograph_debug.
Safe for concurrent use from any number of goroutines.
func (*Graph[N, W]) ApplyVersionedCtx ¶ added in v0.11.0
ApplyVersionedCtx is Graph.ApplyVersioned with the barrier acquisition bounded by ctx. It returns ctx's error — wrapping context.Canceled or context.DeadlineExceeded — without running fn if ctx finishes first, in which case NOTHING is held and no transaction was opened.
Why a writer still needs a deadline (rmp #2306) ¶
The shared hold is uncontended against other ordinary writes, which is the point of rmp #2320. It is NOT uncontended against the exclusive holders: a DDL statement, and an explicit multi-statement transaction that holds the barrier from BEGIN to COMMIT across client think-time. A writer arriving behind one of those waits for its whole tenure.
Before this, that wait ignored the caller's context entirely, and the measurement is the reason this exists: with one explicit transaction open, an autocommit write carrying a 200 ms deadline blocked for TEN MINUTES and returned only when the test harness killed it. Retiring [Engine.writeMu] did not fix that — it moved the same unbounded wait from the writer mutex onto this barrier, which is exactly the shape rmp #2174 fixed for Graph.LockBarrierCtx and left unfixed here.
rmp #2305 removes the transaction-lifetime hold and with it most of the reason to wait at all. The bound is still owed: a DDL statement legitimately excludes writers for as long as it runs, and a caller with a deadline is entitled to hear about it.
Safe for concurrent use from any number of goroutines.
func (*Graph[N, W]) AwaitCommitQuiescence ¶ added in v0.11.0
AwaitCommitQuiescence blocks until every commit timestamp this graph has allocated has been published or abandoned — until MVCCStats.InFlightCommits would read zero — or until ctx finishes.
It is the counterpart obligation to Graph.AllocateCommitTS, and it exists for the one observer that cannot tolerate the window that method opens: a durable checkpoint, which pairs a WAL DURABILITY position with an MVCC VISIBILITY position and truncates the WAL prefix the first one names. A transaction between its fsync and its publish sits below the durable offset and above the visible frontier, so the image does not carry it and the truncation destroys it — an acknowledged commit lost (rmp #2349). Waiting here makes the two positions describe the same set of transactions.
The wait is on the OBSERVER, never on the committer: a writer that is not observed pays nothing, which is the whole reason the durability and visibility steps are not held under one lock. See mvcc.Clock.AwaitQuiescent for the prior art this follows and for the reference engine that chose the other route.
It returns immediately on a graph whose versioning substrate is disarmed, which allocates no timestamps at all.
Concurrency: safe for concurrent use. It takes no lock the write path takes, so a caller may hold a write-admission gate closed across it — and the intended caller does, which is what bounds the wait.
func (*Graph[N, W]) BeginRead ¶ added in v0.11.0
BeginRead opens a read view at the current instant and registers it with the reclamation horizon, so no version this read can still reach is freed while it runs.
The caller MUST pass the result to Graph.EndRead exactly once. Failing to do so holds the watermark for the life of the process.
It returns nil when versioning is disarmed, which is the correct "read the current value" snapshot and costs nothing.
Safe for concurrent use.
Example ¶
ExampleGraph_View shows the recommended way to read a graph that may be mutated concurrently: wrap a multi-op transaction in lpg.Graph.ApplyAtomically / ExampleGraph_BeginRead demonstrates ATOMIC VISIBILITY: a multi-op transaction becomes observable all at once, and a reader that pins a SNAPSHOT never sees it half-applied.
The snapshot is what provides this, and it is the only thing that does (rmp #2344). Per-operation accessors are individually atomic but say nothing about a transaction spanning several substructures; reading through lpg.Graph.ReadAt at an instant taken by lpg.Graph.BeginRead does. Inside the snapshot the cross-substructure invariant "the edge exists iff both endpoint labels exist" always holds. A snapshot read takes NO LOCK, so it neither blocks writers nor is blocked by them.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)
func main() {
g := lpg.New[string, int](adjlist.Config{Directed: true})
// One transaction establishes a cross-substructure invariant: the edge
// alice->bob and both endpoint :Hot labels become visible together.
_ = g.ApplyAtomically(func() error {
_ = g.AddEdge("alice", "bob", 0)
_ = g.SetNodeLabel("alice", "Hot")
_ = g.SetNodeLabel("bob", "Hot")
return nil
})
// Pin an instant. Every read below resolves at it, however many writers
// commit meanwhile.
snap := g.BeginRead()
defer g.EndRead(snap)
v := g.ReadAt(snap)
edge := v.Raw().AdjList().HasEdge("alice", "bob")
srcHot := v.HasNodeLabel("alice", "Hot")
dstHot := v.HasNodeLabel("bob", "Hot")
// The invariant "edge <=> src:Hot <=> dst:Hot": all three observations
// agree, so the set of distinct values has size one.
consistent := edge == srcHot && srcHot == dstHot
fmt.Println("edge:", edge, "src:Hot:", srcHot, "dst:Hot:", dstHot)
fmt.Println("invariant holds:", consistent)
}
Output: edge: true src:Hot: true dst:Hot: true invariant holds: true
func (*Graph[N, W]) BeginVersionedTx ¶ added in v0.11.0
BeginVersionedTx opens a write transaction that OUTLIVES a single statement, for a caller that runs several statements as one transaction — the Cypher engine's explicit transaction ([cypher.Engine.BeginTx]).
What it deliberately does NOT do — rmp #2305 ¶
It takes NO LOCK. Until rmp #2305 an explicit write transaction acquired the schema barrier EXCLUSIVELY at BEGIN and held it until COMMIT or ROLLBACK, across client network round-trips and think-time. Over Bolt that meant one client which sent BEGIN and then stopped talking blocked EVERY other writer in the process for as long as its transaction stayed open. The audit called it the most consequential single fact in it, and the reason is structural: no MVCC engine behaves this way, because an open transaction is supposed to hold VERSIONS, not the engine.
So the lock is not held across the transaction at all. Each statement takes the barrier SHARED for its own duration through Graph.ApplyInVersionedTx, and between statements nothing is held.
What the transaction is, then ¶
It is the commit record. Every version the transaction's statements write is stamped with it, and Graph.EndVersionedTx publishes it ONCE — which is what makes a multi-statement transaction become visible at a single instant, and what makes a rolled-back one leave no trace. Atomicity comes from the record, not from exclusion; that is the whole point of doing this with MVCC.
Contract ¶
The caller MUST close the returned transaction with exactly one call to Graph.EndVersionedTx, on every exit path including a panic, or its horizon slot stays pinned and no version it could reach is ever reclaimed. The returned value MUST be threaded into every write the transaction makes (via Graph.Writer or Graph.ApplyInVersionedTx) and never resolved from the graph's ambient slot: two concurrent explicit transactions overwrite that slot, and reading it would attribute one transaction's writes to the other (rmp #2320's defect class).
Safe for concurrent use from any number of goroutines.
func (*Graph[N, W]) BumpTopoGeneration ¶ added in v0.7.0
func (g *Graph[N, W]) BumpTopoGeneration()
BumpTopoGeneration advances the edge-topology generation counter by one. Deliberately separate from Graph.IncrEdgesAdded / Graph.IncrEdgesRemoved (which bump topoGeneration too, alongside the unrelated TCK side-effect counters): a caller that mutates edge topology WITHOUT an enclosing Cypher statement — a direct store/txn.Store/store/txn.Tx user, bypassing the engine's write adapters entirely — has no Cypher-statement side-effect count to attribute an Incr/Decr to, but the graph's edge topology still changed, so any CSR-position-keyed cache still needs invalidating. Calling this alone leaves edgesAddedCount/edgesRemovedCount untouched, which is correct: those counters answer "how many edges did this Cypher statement add/remove," a question a store-direct write was never part of. Safe for concurrent use.
func (*Graph[N, W]) ChainDepths ¶ added in v0.11.0
ChainDepths returns the retained version-chain depth distribution, summed over every store that keeps chains.
Each store's contribution describes that store's most recent complete sweep; see mvcc.DepthHist for what that means and why it is not an instant.
Safe for concurrent use.
func (*Graph[N, W]) ChainDepthsOf ¶ added in v0.11.0
ChainDepthsOf returns one store's distribution, for a caller that needs to know WHICH structure is holding the long chains.
Safe for concurrent use.
func (*Graph[N, W]) ClearStoreConstraints ¶ added in v0.6.0
func (g *Graph[N, W]) ClearStoreConstraints()
ClearStoreConstraints empties the store-direct constraint set, returning the store-direct count to zero. The cypher engine calls it when it takes ownership of a recovered graph: from that point the engine's own count (SetActiveConstraintCount) is the authoritative source for HasConstraints, so the store-direct count — seeded by recovery for the engine-less case — must not linger and force a checkpoint to over-retain the WAL after the engine later drops a constraint.
ClearStoreConstraints is safe for concurrent use.
func (*Graph[N, W]) ClearStoreIndexes ¶ added in v0.6.0
func (g *Graph[N, W]) ClearStoreIndexes()
ClearStoreIndexes empties the store-direct index set, returning the store-direct count to zero. The cypher engine calls it when it takes ownership of a recovered graph: from that point the engine's own index-def registry is the authoritative source it threads into the checkpoint, so the store-direct count — seeded by recovery for the engine-less case — must not linger and force a checkpoint to over-retain the WAL after the engine later drops an index.
ClearStoreIndexes is safe for concurrent use.
func (*Graph[N, W]) Close ¶ added in v0.11.0
Close releases the background resources this graph owns — currently the MVCC vacuum goroutine — and waits for them to terminate.
It is idempotent and safe to call concurrently with any other operation. The graph remains readable afterwards and writes still record versions; what stops is the sweep, so a caller that closes and then keeps writing accumulates versions with nothing to release them. That is the caller's choice to make, and it is why Close is a shutdown rather than a pause.
A caller that never closes leaks nothing: the vacuum is demand-started and exits on its own once two consecutive passes free nothing. Close exists for the owner that wants the goroutine gone at a known instant — a test that asserts on goroutine counts, or an embedder tearing a graph down while the process lives on. Note that [store.DB] is NOT such an owner: it owns the WAL, the checkpointer and the snapshot writer, and never the in-memory graph, so nothing in the durability stack has a graph to close.
func (*Graph[N, W]) CloseCtx ¶ added in v0.11.0
CloseCtx is Graph.Close with a deadline on the join.
The shutdown SIGNAL is always delivered, whatever ctx says: abandoning it would leave a goroutine running with no way to stop it, which is the leak this method exists to prevent. What ctx bounds is the WAIT for the sweeper to notice — and that wait is already bounded by one pass ([vacuumRecordsPerPass]), so a caller needs a deadline only when it cannot tolerate even that.
It returns ctx.Err() when the deadline passed before the sweeper exited, and nil otherwise. A non-nil error means the goroutine is still winding down, not that the close failed: a later Close or CloseCtx joins it.
The shape mirrors [store.DB.Close] / [store.DB.CloseCtx], which draws the same line between the part of a teardown that must always run and the part a caller may bound.
func (*Graph[N, W]) Config ¶ added in v0.2.0
Config returns the adjlist.Config the graph was constructed with. It delegates to the underlying adjlist.AdjList.Config; the configuration is fixed at New and never mutated, so Config is safe to call concurrently with any other operation and always returns the same value for the lifetime of the graph. The snapshot writer reads it to persist the directed/multigraph shape into the manifest.
func (*Graph[N, W]) DecEdgeCreateCount ¶
func (g *Graph[N, W]) DecEdgeCreateCount(src, dst N)
DecEdgeCreateCount decrements the counter by one (floor 0). Used by Graph.RemoveEdge callers (DELETE) so subsequent MERGEs see the updated multiplicity.
DecEdgeCreateCount is safe for concurrent use.
func (*Graph[N, W]) DecrEdgesAdded ¶ added in v0.2.0
func (g *Graph[N, W]) DecrEdgesAdded()
DecrEdgesAdded subtracts one from the added-edge counter. topoGeneration is NOT decremented — it only ever increases, on the Incr side too, because an undo is itself a topology-changing event for any CSR-position-keyed cache: the graph's content afterward differs from the content the moment before the undo ran, even though it matches the content from further back.
func (*Graph[N, W]) DecrEdgesRemoved ¶ added in v0.2.0
func (g *Graph[N, W]) DecrEdgesRemoved()
DecrEdgesRemoved subtracts one from the removed-edge counter. See Graph.DecrEdgesAdded for why topoGeneration still only ever increases.
func (*Graph[N, W]) DecrNodesAdded ¶ added in v0.2.0
func (g *Graph[N, W]) DecrNodesAdded()
DecrNodesAdded / DecrNodesRemoved / DecrEdgesAdded / DecrEdgesRemoved are the exact inverses of the Incr* counters above. They exist for one purpose: the Cypher executor's transaction-undo path replays the inverse of every eagerly applied mutation when a write query errors or panics, and the per-query side- effect deltas the openCypher TCK asserts (Graph.SideEffectCounters) must not retain the increments of a rolled-back statement. Each subtracts one from the matching monotone counter.
These must only be called to invert a prior Incr* on the same graph; they do not floor at zero, so a stray over-decrement would underflow the unsigned counter. The undo log guarantees one Decr per recorded Incr.
Decr* are safe for concurrent use.
func (*Graph[N, W]) DecrNodesRemoved ¶ added in v0.2.0
func (g *Graph[N, W]) DecrNodesRemoved()
DecrNodesRemoved subtracts one from the removed-node counter.
func (*Graph[N, W]) DelEdgeProperty ¶
DelEdgeProperty removes the named property from the directed edge (src, dst). No-op if absent. The key is cleared on every dst-matching slot so the per-pair view no longer reports it.
func (*Graph[N, W]) DelEdgePropertyByHandle ¶ added in v0.6.0
DelEdgePropertyByHandle removes exactly key from the property bag of the edge identified by handle on the (src, dst) pair, leaving every other property of that handle — and every sibling handle on the same pair — untouched. No-op when handle is 0 (the no-handle sentinel), when either endpoint is unknown to the mapper, when no handle store exists for the pair, or when the handle never carried key. When the removal empties the handle's bag the inner byHandle[handle] entry is pruned, and when that leaves the pair with no handles the outer sh.m[k] entry is pruned too, mirroring the pruning Graph.RemoveEdgeInstanceByHandle performs.
It is the single-key analogue of Graph.RemoveEdgeInstanceByHandle (which drops ALL of a handle's labels and properties): a Cypher REMOVE r.x or SET r.x = null on one parallel edge must delete only x from that one instance, not the whole instance. The per-pair coalesced store is mutated separately by the caller (dual-write); this method only touches the handle-keyed per-instance store.
DelEdgePropertyByHandle is safe for concurrent use.
func (*Graph[N, W]) DelEdgePropertyByHandleID ¶ added in v0.6.0
func (g *Graph[N, W]) DelEdgePropertyByHandleID(srcID, dstID graph.NodeID, handle uint64, key string)
DelEdgePropertyByHandleID removes exactly key from the property bag of the edge identified by `handle` on the directed (srcID, dstID) NodeID pair. It is the NodeID-keyed dual of Graph.DelEdgePropertyByHandle, provided for parity with the other durable NodeID-keyed setters; the WAL recovery path itself uses the natural-key Graph.DelEdgePropertyByHandle because its endpoints are codec-decoded natural keys at replay time. No-op when handle is 0, when the key was never interned, when no handle store exists for the pair, or when the handle never carried key. Empties are pruned exactly as Graph.DelEdgePropertyByHandle prunes them.
DelEdgePropertyByHandleID is safe for concurrent use.
func (*Graph[N, W]) DelNodeProperty ¶
DelNodeProperty removes the named property from n. No-op if absent.
func (*Graph[N, W]) EdgeCreateCount ¶
EdgeCreateCount returns the CREATE multiplicity counter for the directed edge (src, dst), or 0 when no CREATE was recorded.
IT IS AN ALLOCATION SEQUENCE, NOT A QUERYABLE QUANTITY (rmp #2351) ¶
This counter is UNVERSIONED. It is guarded by its own per-shard mutex and is only per-operation atomic; unlike every other per-instance store it has NO as-of form, so it belongs to NO snapshot. Graph.EdgePropertiesAt has Graph.EdgePropertiesAtAsOf and ReadView resolves it at an instant — this has no counterpart, and that asymmetry is the point of this note.
What it exists for is allocating the 1-based instance index a CREATE stamps onto the per-instance stores (Graph.IncEdgeCreateCount, wired by CreateRelationship). For that it need only be monotone per pair, which an atomic counter is. It is not a quantity a reader can correlate with anything at a defined instant.
What that costs a reader, named rather than left to be discovered ¶
A reader that correlates this count with the populated per-instance indices can observe a partial cross-store state — the count already at 2 while only one instance is populated. That used to be OPT-IN: bracketing the correlated reads in Graph.View closed it, because writers committed under an exclusive barrier. rmp #2344 removed Graph.View and the hole is now UNCONDITIONAL, because there is no snapshot to pin the count side to.
ONE production reader does correlate them, and it was checked rather than assumed: cypher/api.go's edgeInstanceIdxFor reads this count beside the snapshot-resolved CSR, and its single caller uses the pair as a CONSERVATIVE GUARD — `parallelCount >= totalCreates && totalCreates > 0` — before trusting a per-instance edge type. A count that is stale-HIGH relative to the reader's snapshot (a concurrent parallel CREATE) makes the guard DECLINE, and the caller falls back to the per-pair union of edge types. So the failure mode is a LOSS OF PRECISION — the pair's union instead of that instance's type — and not a wrong row. That is why this is documented rather than versioned: putting a version chain on a path that currently costs one atomic add, to buy precision in a fallback branch, is not a trade rmp #2338's measurements support.
If a future reader needs this count to be consistent with anything at an instant, it needs an as-of form first. Do not add one speculatively.
See docs/isolation-design.md and TestIsolation_EdgeInstanceStores_CrossStoreRequiresView, whose second half was retracted for exactly this reason.
EdgeCreateCount is safe for concurrent use.
func (*Graph[N, W]) EdgeHasProperty ¶ added in v0.6.0
EdgeHasProperty reports whether the directed edge (src, dst) carries a value under key that would materialise to a NON-NULL Cypher value — without building that value. It is the storage-presence fast path behind a bound relationship's `r.key IS NOT NULL` / `IS NULL` predicate: the caller needs only the boolean presence, so fetching and boxing the value (as Graph.GetEdgeProperty / Graph.EdgeProperties would) is pure waste.
Congruence with the value path is BY CONSTRUCTION, on two axes:
- Per-pair coalescing — it folds parallel edges exactly as Graph.EdgeProperties does: the LATEST dst-matching adjacency slot that carries key wins. The returned answer therefore reflects the same single coalesced value the Cypher evaluator would observe via EdgeProperties, never an earlier shadowed write.
- Kind gating — the winning slot's storage kind is tested with [kindMapsToNonNullCypher], which mirrors cypher.lpgPropToExpr's nullability table. A present-but-null-mapping property (a stored PropTime or PropBytes) reads as Null through Cypher, so this reports false for it, exactly as `r.key IS NOT NULL` would evaluate to false.
The scan reads only validity bits and per-column kind tags (no value cell), so it allocates nothing. Returns false when either endpoint or key is unknown, or when no dst-matching slot carries a non-null-mapping value for key.
Concurrency-safe under the same lock-free contract as Graph.GetEdgeProperty: it reads an immutable published columnar block and bounds its scan by the shorter of the block and the neighbours snapshot, so a concurrent copy-on-write writer is observed atomically (old block or new, never half-built).
func (*Graph[N, W]) EdgeHasPropertyAsOf ¶ added in v0.11.0
EdgeHasPropertyAsOf is Graph.EdgeHasProperty as the edge stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) EdgeIndex ¶
EdgeIndex returns the label index over edges. Edge bitmaps are keyed by the source NodeID; this is suitable for label-filtered out-neighbour scans but not for direct edge enumeration.
func (*Graph[N, W]) EdgeLabels ¶
EdgeLabels returns the names of every label attached to the directed edge (src, dst) in unspecified order. The returned slice is freshly allocated and may be mutated by the caller. If either endpoint is unknown or the endpoint pair has no labels attached, EdgeLabels returns nil.
EdgeLabels is the dual of Graph.NodeLabels. It is safe for concurrent use; the snapshot is taken under the per-shard RWMutex (one of 16 stripes keyed by the src endpoint) and the registry's own lock.
The returned set is DERIVED: the union of the relationship type stored inline in each dst-matching adjacency slot and the per-shard overflow store (the second-and-later types of a multi-label pair and any orphaned types). Distinct labels are deduplicated across both sources, so a multigraph pair whose parallel slots happen to share a type reports it once.
func (*Graph[N, W]) EdgeLabelsAsOf ¶ added in v0.11.0
EdgeLabelsAsOf is Graph.EdgeLabels as the edge stood at s. A nil snapshot reads the current value, which is what a writer inside the barrier needs; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) EdgeLabelsAt ¶
EdgeLabelsAt returns the labels recorded at instance `idx` of the directed edge (src, dst). Returns nil when the instance was never labelled, when either endpoint is unknown, or when no per-instance store has been initialised for this pair.
This per-instance store is guarded by its own per-shard mutex and is only per-operation atomic: it is NOT cross-store consistent with Graph.EdgeCreateCount, Graph.EdgePropertiesAt, or the adjacency layer outside a transaction barrier. A reader correlating this with Graph.EdgeCreateCount while a multi-CREATE multigraph transaction commits can observe a partial cross-store state. To read a consistent cross-store view, take a snapshot with Graph.BeginRead and resolve every correlated read through the ReadView that Graph.ReadAt returns, releasing it with Graph.EndRead. (This used to say "bracket the correlated reads in Graph.View"; that method was removed by rmp #2344 and the advice was left pointing at it — see rmp #2379.) Writers must share ONE transaction record for their writes to land at one instant; see Graph.ApplyAtomically. Also see docs/isolation-design.md.
EdgeLabelsAt is safe for concurrent use.
func (*Graph[N, W]) EdgeLabelsAtAsOf ¶ added in v0.11.0
EdgeLabelsAtAsOf is Graph.EdgeLabelsAt as the instance stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) EdgeLabelsByHandle ¶
EdgeLabelsByHandle returns the labels recorded for the edge identified by handle on the (src, dst) pair. Returns nil when handle is 0, the handle was never labelled, either endpoint is unknown, or no handle store has been initialised for this pair.
Like the (src, dst, idx) instance stores, this handle store is guarded by its own per-shard mutex and is only per-operation atomic: it is NOT cross-store consistent with Graph.EdgeCreateCount, Graph.EdgePropertiesByHandle, or the adjacency layer outside a transaction barrier. To read a consistent cross-store view, take a snapshot with Graph.BeginRead and resolve every correlated read through the ReadView that Graph.ReadAt returns, releasing it with Graph.EndRead. (This used to say "bracket the correlated reads in Graph.View"; that method was removed by rmp #2344 and the advice was left pointing at it — see rmp #2379.) Writers must share ONE transaction record for their writes to land at one instant; see Graph.ApplyAtomically. Also see docs/isolation-design.md.
EdgeLabelsByHandle is safe for concurrent use.
func (*Graph[N, W]) EdgeLabelsByHandleAsOf ¶ added in v0.11.0
EdgeLabelsByHandleAsOf is Graph.EdgeLabelsByHandle as the instance stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) EdgeLabelsByHandleID ¶
EdgeLabelsByHandleID returns the labels recorded for the edge identified by `handle` on the directed (srcID, dstID) NodeID pair, resolving NodeIDs directly rather than through the natural key. It is the NodeID-keyed dual of Graph.EdgeLabelsByHandle used by the snapshot writer, which walks the adjacency by NodeID and must not pay a Resolve→Lookup round trip per handle. Returns nil when handle is 0, the handle was never labelled, or no handle store exists for the pair.
EdgeLabelsByHandleID is safe for concurrent use.
func (*Graph[N, W]) EdgeLabelsByHandleIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) EdgeLabelsByHandleIDAsOf(srcID, dstID graph.NodeID, handle uint64, snap *Snapshot) []string
EdgeLabelsByHandleIDAsOf is Graph.EdgeLabelsByHandleID as the instance stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) EdgeLabelsByID ¶ added in v0.6.0
EdgeLabelsByID is the NodeID-keyed counterpart of Graph.EdgeLabels: it returns the labels attached to the directed edge identified by the endpoint NodeIDs (srcID, dstID), in unspecified order, or nil when the pair carries no labels. It is the edge dual of Graph.NodeLabelsByID.
Unlike Graph.EdgeLabels it performs NO Mapper access — no external-key → NodeID lookup — so a caller that already holds both endpoint NodeIDs can resolve edge labels without re-entering the Mapper. This is precisely what the snapshot collectors require: they enumerate endpoints from inside graph.Mapper.Walk, which holds a Mapper shard read lock across its callback, and the Mapper contract forbids re-entry there while a writer may be running (graph/mapper.go:337-345, #1648). The label snapshot is still taken under the per-shard edge-label RWMutex and the registry's own lock, so EdgeLabelsByID is safe for concurrent use.
func (*Graph[N, W]) EdgeLabelsByIDAsOf ¶ added in v0.11.0
EdgeLabelsByIDAsOf is Graph.EdgeLabelsByID as the edge stood at s.
Safe for concurrent use.
func (*Graph[N, W]) EdgeProperties ¶
func (g *Graph[N, W]) EdgeProperties(src, dst N) map[string]PropertyValue
EdgeProperties returns a snapshot of every property currently attached to the directed edge (src, dst). When several parallel edges connect the pair the result is the latest-wins coalesced union across their slots.
func (*Graph[N, W]) EdgePropertiesAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) EdgePropertiesAsOf(src, dst N, snap *Snapshot) map[string]PropertyValue
EdgePropertiesAsOf is Graph.EdgeProperties as the edge stood at snap.
Safe for concurrent use.
func (*Graph[N, W]) EdgePropertiesAt ¶
func (g *Graph[N, W]) EdgePropertiesAt(src, dst N, idx int64) map[string]PropertyValue
EdgePropertiesAt returns the property map recorded at instance `idx` of the directed edge (src, dst). Returns nil when the instance was never written or when either endpoint is unknown.
This per-instance store is guarded by its own per-shard mutex and is only per-operation atomic: it is NOT cross-store consistent with Graph.EdgeCreateCount, Graph.EdgeLabelsAt, or the adjacency layer outside a transaction barrier. A reader correlating the count of populated instance indices with Graph.EdgeCreateCount while a multi-CREATE multigraph transaction commits can observe a partial cross-store state (count ahead of the populated indices). To read a consistent cross-store view, take a snapshot with Graph.BeginRead and resolve every correlated read through the ReadView that Graph.ReadAt returns, releasing it with Graph.EndRead. (This used to say "bracket the correlated reads in Graph.View"; that method was removed by rmp #2344 — see rmp #2379.) Writers must share ONE transaction record for their writes to land at one instant; see Graph.ApplyAtomically. Also see docs/isolation-design.md.
EdgePropertiesAt is safe for concurrent use.
func (*Graph[N, W]) EdgePropertiesAtAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) EdgePropertiesAtAsOf(src, dst N, idx int64, snap *Snapshot) map[string]PropertyValue
EdgePropertiesAtAsOf is Graph.EdgePropertiesAt as the instance stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) EdgePropertiesByHandle ¶
func (g *Graph[N, W]) EdgePropertiesByHandle(src, dst N, handle uint64) map[string]PropertyValue
EdgePropertiesByHandle returns the property map recorded for the edge identified by handle on the (src, dst) pair. Returns nil when handle is 0, the handle was never written, or either endpoint is unknown.
Like the (src, dst, idx) instance stores, this handle store is guarded by its own per-shard mutex and is only per-operation atomic: it is NOT cross-store consistent with Graph.EdgeCreateCount, Graph.EdgeLabelsByHandle, or the adjacency layer outside a transaction barrier. To read a consistent cross-store view, take a snapshot with Graph.BeginRead and resolve every correlated read through the ReadView that Graph.ReadAt returns, releasing it with Graph.EndRead. (This used to say "bracket the correlated reads in Graph.View"; that method was removed by rmp #2344 and the advice was left pointing at it — see rmp #2379.) Writers must share ONE transaction record for their writes to land at one instant; see Graph.ApplyAtomically. Also see docs/isolation-design.md.
EdgePropertiesByHandle is safe for concurrent use.
func (*Graph[N, W]) EdgePropertiesByHandleAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) EdgePropertiesByHandleAsOf(src, dst N, handle uint64, snap *Snapshot) map[string]PropertyValue
EdgePropertiesByHandleAsOf is Graph.EdgePropertiesByHandle as the instance stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) EdgePropertiesByHandleID ¶
func (g *Graph[N, W]) EdgePropertiesByHandleID(srcID, dstID graph.NodeID, handle uint64) map[string]PropertyValue
EdgePropertiesByHandleID returns the property map recorded for the edge identified by `handle` on the directed (srcID, dstID) NodeID pair. It is the NodeID-keyed dual of Graph.EdgePropertiesByHandle used by the snapshot writer. Returns nil when handle is 0, the handle was never written, or no handle store exists for the pair.
EdgePropertiesByHandleID is safe for concurrent use.
func (*Graph[N, W]) EdgePropertiesByHandleIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) EdgePropertiesByHandleIDAsOf(srcID, dstID graph.NodeID, handle uint64, snap *Snapshot) map[string]PropertyValue
EdgePropertiesByHandleIDAsOf is Graph.EdgePropertiesByHandleID as the instance stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) EdgePropertiesByID ¶ added in v0.6.0
func (g *Graph[N, W]) EdgePropertiesByID(srcID, dstID graph.NodeID) map[string]PropertyValue
EdgePropertiesByID is the NodeID-keyed counterpart of Graph.EdgeProperties: it returns the latest-wins coalesced property map of the directed edge identified by the endpoint NodeIDs (srcID, dstID), or nil when the pair carries no properties. It is the edge dual of Graph.NodePropertiesByID.
Unlike Graph.EdgeProperties it performs NO Mapper access — no external-key → NodeID lookup — so a caller that already holds both endpoint NodeIDs can resolve edge properties without re-entering the Mapper. This is precisely what the snapshot collectors require: they enumerate endpoints from inside graph.Mapper.Walk, which holds a Mapper shard read lock across its callback, and the Mapper contract forbids re-entry there while a writer may be running (graph/mapper.go:337-345, #1648). The read is served from the lock-free immutable adjacency entry, so EdgePropertiesByID is safe for concurrent use.
func (*Graph[N, W]) EdgePropertiesByIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) EdgePropertiesByIDAsOf(srcID, dstID graph.NodeID, snap *Snapshot) map[string]PropertyValue
EdgePropertiesByIDAsOf is Graph.EdgePropertiesByID as the edge stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) EdgeSideVersionCount ¶ added in v0.11.0
EdgeSideVersionCount returns the number of live per-edge side-store version records: overflow relationship types plus per-handle types and properties.
Safe for concurrent use.
func (*Graph[N, W]) EdgeWeight ¶ added in v0.2.0
EdgeWeight returns the weight of the first edge from src to dst and true when such an edge exists, or the zero weight and false otherwise. When several parallel edges connect the pair it returns the weight of the first slot, which is sufficient for the executor's transaction-undo path: it captures the weight of an edge before a failed write query removes it so the inverse Graph.AddEdge restores the same weight.
EdgeWeight performs an O(out-degree) scan of src's adjacency and allocates nothing. It is safe for concurrent use under the same lock-free adjacency snapshot contract as adjlist.AdjList.LoadEntry.
For a weightless graph (adjlist.Config.Weightless) the adjacency carries no weights column, so a present edge reports the zero value of W with ok=true.
func (*Graph[N, W]) EdgeWeightAsOf ¶ added in v0.11.0
EdgeWeightAsOf is Graph.EdgeWeight as the pair stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) EnableLabelDeltas ¶ added in v0.11.0
func (g *Graph[N, W]) EnableLabelDeltas()
EnableLabelDeltas arms the P0 MVCC spike for node labels (rmp #2275).
It exists so both arms can be measured in ONE process toggled by an option rather than by two builds compared back to back, which on this machine has manufactured phantom regressions from a byte-identical control.
It must be called before any label is written and never concurrently with another operation on g. Nothing in the module calls it; it is a measurement seam, and the spike it arms is not a supported feature.
Not safe for concurrent use.
func (*Graph[N, W]) EnablePropDeltas ¶ added in v0.11.0
func (g *Graph[N, W]) EnablePropDeltas()
EnablePropDeltas arms the node-property half of the MVCC substrate.
Separate from Graph.EnableLabelDeltas on purpose: the phases land one structure at a time, and each must be measurable on its own before the next is wired. Must be called before any property is written and never concurrently with another operation on g.
Not safe for concurrent use.
func (*Graph[N, W]) EndRead ¶ added in v0.11.0
EndRead releases a read view obtained from Graph.BeginRead.
It tolerates a nil snapshot, so a caller can defer it unconditionally.
Safe for concurrent use.
func (*Graph[N, W]) EndVersionedTx ¶ added in v0.11.0
EndVersionedTx closes a transaction opened with Graph.BeginVersionedTx: it publishes the transaction's commit record — making every version its statements wrote visible at ONE instant — or, if the transaction was doomed, marks the record aborted so none of them ever becomes visible. It then returns the transaction's horizon slot and recycles its state.
It is idempotent for the zero value and for a graph whose versioning substrate is disarmed, so a caller may invoke it unconditionally on its teardown path.
The publish runs under a SHARED hold on the schema barrier, matching every other write bracket, so a commit cannot land in the middle of a DDL transition. The hold is uncancellable: once a transaction's statements have applied, abandoning the publish would leave the record neither published nor aborted, which stalls the contiguous commit frontier permanently.
Calling it exactly once per Graph.BeginVersionedTx is the caller's obligation. Twice would return an already-returned horizon slot and corrupt the reclamation watermark for every other transaction; never at all pins the slot forever.
func (*Graph[N, W]) EntryViewAsOf ¶ added in v0.11.0
EntryViewAsOf returns every column of id's adjacency entry as it stood at s, resolved from ONE entry so the columns are mutually consistent.
Safe for concurrent use.
func (*Graph[N, W]) FirstEdgeHandle ¶ added in v0.2.0
FirstEdgeHandle returns the stable handle stamped on the FIRST adjacency slot from src to dst — the slot a subsequent Graph.RemoveEdge would remove, because adjlist.AdjList.RemoveEdge removes the lowest-indexed occurrence and compacts the handle column in lock-step. The boolean reports whether such a slot exists AND carries a non-zero handle; it is false when either endpoint is unknown, no src→dst edge exists, or the matched slot has the 0 "no handle" sentinel (a simple-graph or pre-Stage-2 edge).
It lets the write-query transaction-undo log capture the identity of the exact parallel edge instance a DELETE is about to remove, so the inverse can re-add that instance with its ORIGINAL handle (via Graph.AddEdgeHIfAbsent) and the surviving siblings keep theirs — fully reverting an "remove one parallel edge, then fail a later row" rollback without renumbering any handle. See cypher/undo_record.go.
FirstEdgeHandle reads an immutable adjacency snapshot (adjlist.AdjList.LoadEntryH) and allocates nothing; it is safe for concurrent use under the same lock-free contract as Graph.EdgeWeight.
func (*Graph[N, W]) FirstEdgeHandleAsOf ¶ added in v0.11.0
FirstEdgeHandleAsOf is Graph.FirstEdgeHandle as the pair stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) ForEachEdgeLabelByID ¶ added in v0.6.0
ForEachEdgeLabelByID streams the distinct labels of the directed edge (src, dst), invoking visit once per resolved label name without materialising the []string that Graph.EdgeLabelsByID returns. It is the allocation-fusing counterpart of EdgeLabelsByID — the edge-label analogue of Graph.ForEachNodeLabelByID — chiefly for the snapshot writer.
The distinct label ids (inline slots + overflow, deduplicated) are gathered under the edge-label shard read lock; names are resolved and visited after the lock is released, exactly as EdgeLabelsByID does, so visit may safely read the graph. The dedup scratch is the same small per-call slice EdgeLabelsByID uses; the saving is the []string result slice the caller would otherwise range over.
func (*Graph[N, W]) ForEachEdgeLabelByIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) ForEachEdgeLabelByIDAsOf(srcID, dstID graph.NodeID, snap *Snapshot, visit func(name string))
ForEachEdgeLabelByIDAsOf is Graph.ForEachEdgeLabelByID as the edge stood at snap.
Safe for concurrent use.
func (*Graph[N, W]) ForEachEdgeProperty ¶ added in v0.6.0
func (g *Graph[N, W]) ForEachEdgeProperty(src, dst N, visit func(name string, pv PropertyValue))
ForEachEdgeProperty streams the latest-wins coalesced property set of the directed edge (src, dst), invoking visit once per emitted (name, value) without building the intermediate per-pair map that Graph.EdgeProperties returns. It is the allocation-fusing counterpart of Graph.EdgeProperties, the edge analogue of Graph.NodePropertiesByIDFunc: a caller that re-keys every property into a different map (chiefly the Cypher result path, which converts each lpg.PropertyValue into a cypher/expr value) would otherwise allocate a throwaway map[string]PropertyValue only to range over it once. Streaming the values lets the caller build its target map directly, removing that intermediate allocation per relationship row.
visit is called zero times when either endpoint is unknown or the pair carries no properties. See Graph.ForEachEdgePropertyByID for the coalescing and concurrency contract.
func (*Graph[N, W]) ForEachEdgePropertyAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) ForEachEdgePropertyAsOf(src, dst N, snap *Snapshot, visit func(name string, pv PropertyValue))
ForEachEdgePropertyAsOf is Graph.ForEachEdgeProperty as the edge stood at snap.
Safe for concurrent use.
func (*Graph[N, W]) ForEachEdgePropertyByID ¶ added in v0.6.0
func (g *Graph[N, W]) ForEachEdgePropertyByID(srcID, dstID graph.NodeID, visit func(name string, pv PropertyValue))
ForEachEdgePropertyByID is the NodeID-keyed counterpart of Graph.ForEachEdgeProperty and the streaming counterpart of Graph.EdgePropertiesByID: it invokes visit once per (name, value) of the latest-wins coalesced property set of the edge identified by the endpoint NodeIDs (srcID, dstID), without materialising the intermediate map.
Like Graph.EdgePropertiesByID it performs NO Mapper access, so a caller that already holds both endpoint NodeIDs avoids re-entering the Mapper.
Coalescing: the per-pair view folds parallel edges by taking the LATEST dst-matching adjacency slot per key. Because the columns are per-slot and a key is present in at most one column per slot, visit fires at most once per name PER SLOT; across the parallel slots a name MAY be visited more than once, so a consumer that needs the single coalesced value MUST apply last-write-wins (the last emission for a name is the coalesced winner, exactly as Graph.EdgePropertiesByID records out[name] = v). A map-building consumer gets this for free.
Concurrency-safe under the same lock-free contract as Graph.EdgePropertiesByID: it reads an immutable, atomically-published columnar block and neighbours snapshot and bounds the scan by the shorter of the two, so a concurrent copy-on-write writer is observed atomically (old snapshot or new, never half-built). Unlike Graph.NodePropertiesByIDFunc NO lock is held across visit — the reads are lock-free atomic-pointer loads — so visit imposes no re-entrancy restriction. The PropertyValue passed to visit is a value copy of the immutable cell, so copying it out (or deriving an independent value from it) is safe; for the boxed Bytes/List kinds the same slice-aliasing caveat as Graph.GetEdgeProperty applies.
func (*Graph[N, W]) ForEachEdgePropertyByIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) ForEachEdgePropertyByIDAsOf(srcID, dstID graph.NodeID, snap *Snapshot, visit func(name string, pv PropertyValue))
ForEachEdgePropertyByIDAsOf is Graph.ForEachEdgePropertyByID as the edge stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
It resolves the neighbours and the columnar block from ONE entry, so the two are the same version. The previous form loaded them separately and bounded the scan by the shorter length, which kept the index in range but did not make the columns agree — adequate under the barrier, not under a snapshot.
Safe for concurrent use.
func (*Graph[N, W]) ForEachNodeLabelByID ¶ added in v0.6.0
ForEachNodeLabelByID streams the labels of the node identified by id, invoking visit once per resolved label name without materialising the []string that Graph.NodeLabelsByID returns. It is the allocation-fusing counterpart of NodeLabelsByID — the label analogue of Graph.NodePropertiesByIDFunc — chiefly for the snapshot writer, which re-keys every label into its own string table and would otherwise allocate a throwaway slice per node.
Concurrency: visit runs while the node-label shard's read lock is held, so it observes a consistent snapshot of the node's labels relative to any concurrent writer holding the shard write lock — identical to Graph.NodeLabelsByID. visit therefore MUST NOT call back into any Graph method that takes a node-label-shard lock (it would deadlock); copying the name string out is safe.
func (*Graph[N, W]) ForEachNodeLabelByIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) ForEachNodeLabelByIDAsOf(id graph.NodeID, snap *Snapshot, visit func(name string))
ForEachNodeLabelByIDAsOf is Graph.ForEachNodeLabelByID as the node stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
The bag is resolved BEFORE visit runs and no shard lock is held across it, which the plain form could not offer: a reconstructed version is a private copy, so there is nothing left to protect.
Safe for concurrent use.
func (*Graph[N, W]) ForEachPairOverflowRelTypeByID ¶ added in v0.11.0
func (g *Graph[N, W]) ForEachPairOverflowRelTypeByID(srcID, dstID graph.NodeID, visit func(name string))
ForEachPairOverflowRelTypeByID streams the directed pair's OVERFLOW relationship types, in list order, invoking visit once per resolved name.
The overflow list holds a type Graph.SetEdgeLabel could not place per slot because no column-typed slot of the pair was free; naming the pair named every one of them, so [Graph.slotCarriesType] reads an overflow type as carried by every column-typed slot of the pair. It is the second half of a pair's durable type state, and it is per-PAIR rather than per-slot by construction.
It short-circuits on [Graph.edgeLabelOverflowActive], so a graph with no overflow anywhere — every Cypher-built one, and every graph whose pairs never needed a second type — pays one atomic load and takes no lock.
ForEachPairOverflowRelTypeByID is safe for concurrent use. Names are resolved after the shard lock is released, so visit may safely read the graph.
func (*Graph[N, W]) ForEachPairOverflowRelTypeByIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) ForEachPairOverflowRelTypeByIDAsOf(srcID, dstID graph.NodeID, s *Snapshot, visit func(name string))
ForEachPairOverflowRelTypeByIDAsOf is Graph.ForEachPairOverflowRelTypeByID resolving the pair's overflow relationship types AS OF s, rather than as of the present (rmp #2310).
A nil snapshot reads the current stored value, which is what the unversioned form does, so a caller may pass whatever it has.
It exists because a checkpoint capture must read every structure at ONE transactional instant while writers keep committing. The unversioned form takes the shard read lock and reports what the pair holds NOW, which is the right answer for a caller inside the visibility barrier and the wrong one for a capture that no longer excludes writers.
Safe for concurrent use.
func (*Graph[N, W]) ForEachPairSlotRelTypeByID ¶ added in v0.11.0
func (g *Graph[N, W]) ForEachPairSlotRelTypeByID( srcID, dstID graph.NodeID, visit func(ordinal int, name string), )
ForEachPairSlotRelTypeByID streams the INLINE relationship type of each slot of the directed pair (srcID → dstID), in canonical ordinal order, invoking visit once per typed slot with that slot's ordinal and type name. A slot whose label column entry is the 0 sentinel carries no inline type and is not visited, so a pair of two slots where only the second is typed yields exactly one call, with ordinal 1.
It reports the slot's OWN entry and nothing else: not a sibling slot's entry, not the pair's overflow list (that is Graph.ForEachPairOverflowRelTypeByID), and not the by-handle store (that has its own durable component). Reporting the pair's derived union instead is what made a checkpoint round trip lose a type on one shape and invent one on another (rmp #2262).
It takes NO Mapper lock and performs no external-key lookup, so the snapshot writer may call it after snapshotting node ids inside graph.Mapper.Walk without re-entering the Mapper (#1648).
ForEachPairSlotRelTypeByID is safe for concurrent use; it observes a lock-free adjacency snapshot, so a slot added concurrently may or may not be included.
func (*Graph[N, W]) ForEachPairSlotRelTypeByIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) ForEachPairSlotRelTypeByIDAsOf( srcID, dstID graph.NodeID, s *Snapshot, visit func(ordinal int, name string), )
ForEachPairSlotRelTypeByIDAsOf is Graph.ForEachPairSlotRelTypeByID resolving the pair's per-slot relationship types AS OF s rather than as of the present (rmp #2310).
A nil snapshot reads the live entry, so it is exactly the unversioned form.
It exists for the checkpoint capture, which reads every structure at one transactional instant while writers keep committing. The unversioned form reads the LIVE entry through LoadEntryH/LoadEntryLabels; the versioned one takes ONE entry view as of s and reads the neighbour and label columns out of it, so the two columns cannot come from different instants — which they can in the live form, where a commit landing between the two loads leaves the ordinals and the types describing different states of the same pair.
Safe for concurrent use.
func (*Graph[N, W]) ForEachSlotRelTypeByID ¶ added in v0.11.0
func (g *Graph[N, W]) ForEachSlotRelTypeByID(srcID, dstID graph.NodeID, encoded uint32, visit func(name string))
ForEachSlotRelTypeByID streams the relationship types carried by ONE column-typed adjacency slot of the directed pair (srcID → dstID) — the slot whose own label column entry is encoded — invoking visit once per resolved type name. encoded is the raw column value: 0 means the slot carries no inline type.
It is the per-SLOT counterpart of Graph.ForEachEdgeLabelByID, which streams the pair's whole derived union. A caller that must decide what ONE parallel edge is typed as cannot use the union: on a multigraph pair holding one :K edge and one untyped edge the union reports :K for both, so a pattern `()-[r:K]->()` matched twice where once was correct (rmp #2258).
A column-typed slot carries its own inline type plus every type in the pair's overflow list. Overflow holds a type Graph.SetEdgeLabel could not place per-slot because no slot was free; SetEdgeLabel names the PAIR, so such a type belongs to every column-typed slot of it. Nothing else is visited — in particular, no sibling slot's inline type — which is what makes the answer per-slot.
This is the accessor for a slot whose type is NOT recorded against a stable per-edge handle. When a handle record exists it is authoritative for that slot and Graph.EdgeLabelsByHandleID is the accessor to use; see [Graph.slotCarriesType], which applies the same precedence.
Names are resolved and visited after the edge-label shard read lock is released, exactly as ForEachEdgeLabelByID does, so visit may safely read the graph. It allocates nothing when the pair has no overflow, which is the case for every Cypher-built graph.
ForEachSlotRelTypeByID is safe for concurrent use.
func (*Graph[N, W]) ForEachSlotRelTypeByIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) ForEachSlotRelTypeByIDAsOf(srcID, dstID graph.NodeID, encoded uint32, snap *Snapshot, visit func(name string))
ForEachSlotRelTypeByIDAsOf is Graph.ForEachSlotRelTypeByID as the pair's overflow stood at snap.
Safe for concurrent use.
func (*Graph[N, W]) GetEdgeProperty ¶
func (g *Graph[N, W]) GetEdgeProperty(src, dst N, key string) (PropertyValue, bool)
GetEdgeProperty returns the property value attached to the directed edge (src, dst) under key. When several parallel edges connect the pair the latest-winning value across their slots is returned (the slots carry the identical value by the SetEdgeProperty fan-out, so this is well-defined).
func (*Graph[N, W]) GetEdgePropertyAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) GetEdgePropertyAsOf(src, dst N, key string, snap *Snapshot) (PropertyValue, bool)
GetEdgePropertyAsOf is Graph.GetEdgeProperty as the edge stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) GetNodeProperty ¶
func (g *Graph[N, W]) GetNodeProperty(n N, key string) (PropertyValue, bool)
GetNodeProperty returns the property value attached to n under key, and a bool reporting whether the property is set.
func (*Graph[N, W]) GetNodePropertyAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) GetNodePropertyAsOf(n N, key string, s *Snapshot) (PropertyValue, bool)
GetNodePropertyAsOf is Graph.NodePropertyByIDAsOf keyed by the external node key.
Safe for concurrent use.
func (*Graph[N, W]) HasConstraints ¶ added in v0.3.0
HasConstraints reports whether the cypher engine currently has any schema constraint registered on this graph. It reads a lock-free counter maintained by the engine (SetActiveConstraintCount), so it is cheap enough for the checkpointer to consult on every checkpoint to gate the constraints.bin self-sufficiency requirement (#1464).
HasConstraints is safe for concurrent use.
It reports true when EITHER the engine-maintained count (SetActiveConstraintCount) OR the store-direct count (AddStoreConstraint, maintained by the txn.Store apply path) is positive, so the checkpoint fail-safe is correct whether the constraint was declared through the cypher engine or directly through txn.Tx.CreateConstraint (#1756).
func (*Graph[N, W]) HasEdgeAsOf ¶ added in v0.11.0
HasEdgeAsOf is Graph.HasEdgeByIDAsOf keyed by the external node keys.
Safe for concurrent use.
func (*Graph[N, W]) HasEdgeByIDAsOf ¶ added in v0.11.0
HasEdgeByIDAsOf reports whether a directed edge srcID→dstID existed at s.
Safe for concurrent use.
func (*Graph[N, W]) HasEdgeHandle ¶
HasEdgeHandle reports whether the directed (src, dst) pair carries a stored edge whose stable handle equals `handle`. It scans the pair's parallel handle column on the adjacency slot — the single source of truth for which handles are live on which pair — and returns false when handle is 0 (the no-handle sentinel), when either endpoint is unknown to the mapper, or when the pair has no slot stamped with that handle.
HasEdgeHandle is the idempotency predicate WAL replay uses: an OpAddEdgeH whose handle is already present (loaded from the snapshot or applied by an earlier frame) is a no-op, so snapshot + full-WAL recovery does not double the edge.
HasEdgeHandle is safe for concurrent use.
func (*Graph[N, W]) HasEdgeHandleLabelRecordByID ¶ added in v0.11.0
HasEdgeHandleLabelRecordByID reports whether the by-handle label store holds a relationship-type record for handle on the directed (srcID, dstID) pair.
It is the presence question on its own, without the types: a slot WITHOUT a record is COLUMN-TYPED — its relationship type lives in the adjacency label column and Graph.ForEachSlotRelTypeByID is what reads it — while a slot WITH one has the record as its authority. [Graph.slotCarriesType] applies exactly that precedence, so a caller classifying slots must ask the same question or the two will disagree about which source owns a slot.
It exists because the alternative probe — calling Graph.EdgeLabelsByHandle and testing the result for emptiness — resolves every id to a name and allocates a []string per slot, which a per-slot classification sweep over a whole graph cannot pay. This allocates nothing and takes one map lookup under the pair's shard lock. It also needs no Mapper round-trip, taking NodeIDs directly.
HasEdgeHandleLabelRecordByID is safe for concurrent use.
func (*Graph[N, W]) HasEdgeHandleLabelRecordByIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) HasEdgeHandleLabelRecordByIDAsOf(srcID, dstID graph.NodeID, handle uint64, snap *Snapshot) bool
HasEdgeHandleLabelRecordByIDAsOf is Graph.HasEdgeHandleLabelRecordByID as the store stood at snap. The precedence it feeds — a slot WITH a record is handle-typed, one WITHOUT is column-typed — must be resolved at the reader's own instant, or a reader from before a CREATE would classify a slot by a record that did not exist for it.
Safe for concurrent use.
func (*Graph[N, W]) HasEdgeLabel ¶
HasEdgeLabel reports whether the directed edge (src, dst) carries name as a label.
func (*Graph[N, W]) HasEdgeLabelAsOf ¶ added in v0.11.0
HasEdgeLabelAsOf is Graph.HasEdgeLabel as the edge stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) HasIndexes ¶ added in v0.6.0
HasIndexes reports whether any secondary index has been declared through the txn.Store-direct API on this graph. It reads a lock-free counter maintained by the txn.Store apply path (AddStoreIndex / RemoveStoreIndex), so it is cheap enough for the checkpointer to consult on every checkpoint to gate the indexdefs.bin self-sufficiency requirement: a checkpoint that truncates the WAL prefix which first declared an index must carry the index definition in the snapshot, or the index is silently lost on the next reopen (#1755).
It reports true when EITHER the engine-maintained count (SetActiveIndexCount) OR the store-direct count (AddStoreIndex, maintained by the txn.Store apply path) is positive, so the checkpoint fail-safe is correct whether the index was declared through the cypher engine or directly through txn.Tx.CreateIndex (#1755). Two sources are required because the engine's CREATE INDEX commits via Tx.CommitWALOnly, which never replays through the store apply path, so storeIndexActive alone would be blind to every engine-declared index.
HasIndexes is safe for concurrent use.
func (*Graph[N, W]) HasNodeLabel ¶
HasNodeLabel reports whether n carries the named label.
func (*Graph[N, W]) HasNodeLabelAsOf ¶ added in v0.11.0
HasNodeLabelAsOf is Graph.HasNodeLabelByIDAsOf keyed by the external node key.
Safe for concurrent use.
func (*Graph[N, W]) HasNodeLabelByID ¶ added in v0.6.0
HasNodeLabelByID is the NodeID-keyed, allocation-free counterpart of Graph.HasNodeLabel: it reports whether the node identified by id carries the named label without the external-key → NodeID Mapper lookup and without materialising the node's label slice (which [NodeLabelsByID] would).
It backs the lazy `n:Label` predicate fast path in the Cypher engine, which holds the NodeID already and only needs a membership test. An unknown label name (never interned) is a definite "absent" answer, mirroring Graph.HasNodeLabel.
func (*Graph[N, W]) HasNodeLabelByIDAsOf ¶ added in v0.11.0
HasNodeLabelByIDAsOf reports whether id carried the named label at s.
Safe for concurrent use.
func (*Graph[N, W]) Horizon ¶ added in v0.11.0
Horizon returns the reader horizon this graph reclaims against.
A reader registers its start timestamp with it for as long as it is active, so reclamation can tell which versions are still reachable. It is exported because the layer that owns a read's lifetime — the Cypher engine — lives in another package.
Safe for concurrent use.
func (*Graph[N, W]) IncEdgeCreateCount ¶
IncEdgeCreateCount bumps the CREATE multiplicity counter for the directed edge (src, dst) by one. Returns the new count.
Idempotent across "edge already exists" calls: simple-graph upsertEdge no-ops the underlying storage write, but the counter still moves so a subsequent MERGE sees the correct multiplicity.
IncEdgeCreateCount is safe for concurrent use.
func (*Graph[N, W]) IncrEdgesAdded ¶
func (g *Graph[N, W]) IncrEdgesAdded()
IncrEdgesAdded records that one edge was freshly added.
func (*Graph[N, W]) IncrEdgesRemoved ¶
func (g *Graph[N, W]) IncrEdgesRemoved()
IncrEdgesRemoved records that one edge was removed.
func (*Graph[N, W]) IncrNodesAdded ¶
func (g *Graph[N, W]) IncrNodesAdded()
IncrNodesAdded / IncrNodesRemoved / IncrEdgesAdded / IncrEdgesRemoved expose the per-direction counters to the cypher executor so the mutator adapters can record each event as it happens. The graph itself does not call these — node and edge mutation flow through the adapters, which know whether a given AddNode/AddEdge was a fresh allocation or a no-op re-intern. IncrNodesAdded records that one node was freshly added.
func (*Graph[N, W]) IncrNodesRemoved ¶
func (g *Graph[N, W]) IncrNodesRemoved()
IncrNodesRemoved records that one node was removed.
func (*Graph[N, W]) IndexManager ¶
IndexManager returns the manager of secondary indexes attached to this graph, or nil when no manager has been set. Callers that need snapshot-durable indexes must register them via index.Manager.CreateIndex on a manager set via Graph.SetIndexManager.
IndexManager is safe for concurrent use; the pointer is loaded with sequential consistency.
func (*Graph[N, W]) IndexRemovalBacklog ¶ added in v0.11.0
IndexRemovalBacklog returns how many label-index removals are waiting for the watermark.
It is exported because a deferred removal is memory the substrate is responsible for, and the bounded-resources mandate asks that such things be observable rather than merely bounded.
Safe for concurrent use.
func (*Graph[N, W]) IsTombstoned ¶
IsTombstoned reports whether id has been marked removed via Graph.RemoveNode. Used by the Cypher executor's AllNodesScan to skip phantom nodes (those that the Mapper still indexes but that the graph treats as deleted).
It is a DERIVED ACCELERATOR, not an independent answer (rmp #2311) ¶
The authoritative answer to "does this node exist" is the versioned life store — Graph.NodeExistsAsOf, which resolves the node's birth and death records against a reader's instant. This bitmap answers the same question for the PRESENT ONLY, and it keeps no history, so it cannot answer it as of any other instant at any price.
It survives because it is materially faster and the rule for keeping it was measurement, not preference. BenchmarkExistence, Apple M4, 100k nodes, benchstat n=6:
clean graph bitmap 1.179ns ± 1% versioned 3.279ns ± 1% 2.78x 1 in 8 removed bitmap 5.644ns ± 1% versioned 7.755ns ± 1% 1.37x
2.1 ns per existence test on the common path, on a question asked once per scanned row, with no allocation in either arm.
THE CONTRACT THAT COMES WITH KEEPING IT: it is maintained in lockstep with the death records by Graph.RemoveNode and [Graph.revive], and a caller that needs the answer AS OF a reader's instant must use Graph.NodeExistsAsOf and never this. Where the versioned store has no record — a birth older than every live reader, or one already reclaimed — NodeExistsAsOf itself falls back here, which is exactly the accelerator relationship and not a second source of truth.
func (*Graph[N, W]) LabelBitmapAsOf ¶ added in v0.11.0
LabelBitmapAsOf returns the members of lid's bitmap that carried the label at s, as a bitmap the caller owns.
A nil snapshot, or a graph with nothing deferred and no live label or node history, returns the index's own answer untouched — which is what every read-only workload gets, and it costs one bitmap clone exactly as it did before.
Otherwise every member is re-checked against the versioned label bag and the versioned existence record. That is O(members) once per SCAN, not per row.
Safe for concurrent use.
func (*Graph[N, W]) LabelCountBound ¶ added in v0.11.0
LabelCountBound returns an UPPER BOUND on the number of nodes carrying lid as of s, and whether that bound happens to be exact.
It exists because Graph.LabelCountExact declines the moment any history is live, and a caller that only needs to know whether the cardinality can EXCEED some threshold does not need an exact count — it needs a bound. Declining forced those callers to materialise the filtered bitmap instead, which is far more expensive than the question: rmp #2392 measured a planner gate cloning a 3000-node label bitmap once per query, 4.1 GB over one run of examples/35_mvcc_mixed_workload, purely because a concurrent writer kept deltas live and so made the exact count unavailable. A bound answers that question with three atomic loads and no allocation at all.
Why the bound is sound ¶
Graph.LabelBitmapAsOf starts from the raw index bitmap and corrects it over the SUSPECT set (see [Graph.correctBitmapOver]). A correction either removes a member or adds one, and it visits each suspect once, so it can add at most one member per suspect:
countAsOf(lid, s) ≤ rawCount(lid) + |suspects|
[Graph.suspectNodes] builds that set from exactly three sources — the node-label delta keys, the node-life born/died keys, and the deferred index-removal keys — each of which maintains its own active counter. Summing those three therefore bounds |suspects| without walking a single shard. The sum is deliberately LOOSE: a suspect that removes a member, or that concerns a different label entirely, still counts towards it. Loose in this direction is the safe one — it can only make the bound larger, never smaller than the truth.
exact is true only when no filtering is needed at all, in which case the raw count is returned unchanged and is the same value Graph.LabelCountExact gives. A caller that needs an exact count must still use that method; this one never promises one.
Safe for concurrent use.
func (*Graph[N, W]) LabelCountExact ¶ added in v0.11.0
LabelCountExact returns the number of nodes carrying lid and whether that number is EXACT for s.
It is not exact whenever Graph.LabelBitmapAsOf would have to filter, for the reason given in the file comment: a count has no object to be re-checked against, so the only sound answer is to decline and let the caller count the filtered scan.
Safe for concurrent use.
func (*Graph[N, W]) LabelDeltaCount ¶ added in v0.11.0
LabelDeltaCount returns the number of live node-label delta records.
It is the lock-free gate a reader consults before considering a chain walk: zero means no node in the graph has an unreclaimed older version, which is the whole of a read-only workload. It is also the memory bound the spike reports, since nothing reclaims deltas yet.
Safe for concurrent use.
func (*Graph[N, W]) LabelsBitmapAsOf ¶ added in v0.11.0
LabelsBitmapAsOf is Graph.LabelBitmapAsOf for a conjunction of labels.
Safe for concurrent use.
func (*Graph[N, W]) LabelsCountExact ¶ added in v0.11.0
LabelsCountExact is Graph.LabelCountExact for a CONJUNCTION of labels: the number of nodes carrying every lid, and whether that number is EXACT for s.
It exists because the conjunction count had no as-of form at all. The zero-alloc path — roaring's AndCardinality straight over the live bitmaps — was reached directly, bypassing [Graph.labelBitmapNeedsFilter], so a deferred label removal left the node in both raw bitmaps and the count included a node whose per-row label predicate correctly answered false. The count never evaluates a predicate, so nothing downstream could correct it (rmp #2326).
It CORRECTS rather than declines, which is the difference between this and Graph.LabelCountExact. Declining was tried first and is wrong here: the conjunction count also feeds a planner gate, and refusing it whenever any label history is live made the intersection optimisation never engage at all — TestLabelIntersect_Rapid detected that as a vacuous property. So the zero-alloc AndCardinality is kept for the case where the raw bitmaps are authoritative, and otherwise the answer is the cardinality of the FILTERED conjunction, which costs the same clone the scan on that path pays anyway.
ok is false only when the conjunction is not answerable at all (no labels).
Safe for concurrent use.
func (*Graph[N, W]) LiveCountExactAsOf ¶ added in v0.11.0
LiveCountExactAsOf reports whether the CURRENT live node count is also the count this reader should see.
It is, when no node was born or removed after the reader started: the present and the reader's instant then contain exactly the same nodes, however many records happen to be retained. That is a very different question from "is any record live at all", which is what the first version asked — and asking the weaker one made `MATCH (n) RETURN count(*)` decline its O(1) answer and walk every node under a per-node lock the moment ANY write had happened, which measured 2.5x on BenchmarkEngReadUnderWriter against a saturating writer.
The scan is over the life shards' own maps, which hold only the churn the reclaimer has not caught up with — sixteen uncontended read locks once per QUERY, against one per node.
Safe for concurrent use.
func (*Graph[N, W]) LiveNodeFilter ¶ added in v0.6.0
LiveNodeFilter returns a predicate reporting whether a NodeID is live (not tombstoned), or nil when the graph carries no tombstones at all. It is the liveness argument for [csr.BuildFromAdjListLive]: passing it builds a search CSR that omits the ghost edges left behind by Graph.RemoveNode (which tombstones a node without stripping its incident edges), while the nil return on a tombstone-free graph preserves the zero-overhead build fast path (#1790).
The returned predicate is a point-in-time view: it closes over the graph and re-reads tombstone state on each call, so it must be used against a quiescent graph (the same single state the CSR build snapshots).
func (*Graph[N, W]) LiveOrder ¶
LiveOrder returns the number of non-tombstoned interned nodes.
LiveOrder is safe for concurrent use and takes no lock: tombstoneActive mirrors the published bitmap's cardinality exactly (both move together under tombstoneMu), so the dead count is a single atomic load.
func (*Graph[N, W]) LockBarrier ¶ added in v0.3.0
func (g *Graph[N, W]) LockBarrier()
LockBarrier acquires the graph's transaction-visibility write lock and stamps the calling goroutine as the barrier holder, identical to Graph.ApplyAtomically but split into a manual acquire/release pair for callers that need to hold the barrier across multiple operations (e.g. an explicit multi-statement transaction that must block concurrent readers for its whole lifetime, task #1412).
The caller MUST release the lock with exactly one paired call to Graph.UnlockBarrier, even if an error or panic occurs — failing to do so deadlocks the engine. The typical pattern is:
g.LockBarrier() defer g.UnlockBarrier()
While the lock is held, any operation inside the barrier that needs to run under the same lock (e.g. [Engine.execUnderBarrier] called from an in-flight Exec) MUST use Graph.ApplyInsideLocked instead of Graph.ApplyAtomically; calling ApplyAtomically from the goroutine that holds the barrier via LockBarrier panics (re-entrancy guard) — in a build made with -race or -tags gograph_debug; a released build deadlocks instead, see Graph.ApplyAtomically.
LockBarrier must not be called from a goroutine already inside the barrier (ApplyAtomically or a previous LockBarrier). Under -race or -tags gograph_debug it panics instead of deadlocking; a released build deadlocks. LockBarrier waits for the barrier however long it takes. Prefer Graph.LockBarrierCtx, which bounds the wait by a context.
func (*Graph[N, W]) LockBarrierCtx ¶ added in v0.11.0
LockBarrierCtx is Graph.LockBarrier with the acquisition bounded by ctx. It returns nil once the barrier is held, or ctx's error — wrapping context.Canceled or context.DeadlineExceeded — if ctx finishes first.
On error NOTHING is held and Graph.UnlockBarrier must NOT be called; on nil the caller owns the barrier and must release it exactly once, as with LockBarrier.
The wait exists because a DDL holds the visibility gate strongly for its whole scan-and-register sequence, so a writer arriving mid-DDL queues behind it. Before rmp #2174 that wait was unbounded from the caller's point of view: the round-3 audit measured Engine.BeginTx with a 50 ms deadline returning after 601 ms, and after 11.60 s under load, in both cases with a live transaction and err=nil. See mvcc.Gate.StrongLockCtx and the acquireCtx helper beside it for how the wait is bounded and why a queued acquire cannot simply be abandoned. (It used to say "Graph.View readers hold the barrier's read side"; rmp #2344 removed Graph.View and reads take no barrier at all.)
func (*Graph[N, W]) MVCCStats ¶ added in v0.11.0
MVCCStats returns the current state of the versioning substrate.
Safe for concurrent use.
func (*Graph[N, W]) NewSession ¶ added in v0.11.0
NewSession returns a session bound to this graph, with no commit to wait for.
Safe for concurrent use.
func (*Graph[N, W]) NextEdgeHandle ¶
NextEdgeHandle returns a fresh, never-reused stable edge handle from the per-graph monotone counter (the exported form of [Graph.nextEdgeHandle]). It is used by the transactional store (store/txn) to mint the handle stamped onto a durable OpAddEdgeH WAL frame BEFORE the edge is applied, so the same handle is written to the log and to the in-memory adjacency. Handles start at 1; 0 is the reserved "no handle" sentinel. The counter is re-seeded after recovery via Graph.SeedEdgeHandle so handles stay monotone across a reopen.
NextEdgeHandle is safe for concurrent use.
func (*Graph[N, W]) NodeExistsAsOf ¶ added in v0.11.0
NodeExistsAsOf reports whether id was a live node at s.
A nil snapshot asks about the present, which is the tombstone check the read path has always made.
The rule ¶
A node exists for a reader when its birth is visible to that reader and its death is not. "No record" resolves the same way it does everywhere else here: no birth record means it was born before anything this reader can remember, and no death record means it is not dead.
Safe for concurrent use.
func (*Graph[N, W]) NodeInternedAsOf ¶ added in v0.11.0
NodeInternedAsOf reports whether id had been INTERNED at or before s — that is, whether the node existed at any point up to that instant, whether or not it had already been removed by then.
A nil snapshot reports whether the id is interned at all.
Why this is a different question from NodeExistsAsOf, and who needs it ¶
Graph.NodeExistsAsOf answers "is this node ALIVE as of s". A snapshot image needs a weaker question for its MAPPER: the id→key table must carry every id the image can reference, and that includes ids whose node was removed before s — those are in the mapper AND in the tombstone set, which is how a removal survives a restart.
What the mapper must NOT carry is an id interned AFTER s. Before rmp #2310 that could not happen, because the capture excluded writers and the mapper could not grow during it. It can now, and it was measured the moment the exclusion was removed: TestCheckpoint_CaptureIsAtomic_SnapshotOnlyArtefact recovered Order=322 against Size=157, eight nodes above the 2*Size the fixture guarantees — exactly the endpoints of four transactions that committed during the capture and whose edges were correctly excluded.
A node with no birth record is treated as interned: it predates the versioned life store, or its record has been reclaimed, and in both cases its birth is in the past of every live reader.
Safe for concurrent use.
func (*Graph[N, W]) NodeLabels ¶
NodeLabels returns the names of every label attached to n in unspecified order.
Example ¶
ExampleGraph_NodeLabels shows that a node may carry several labels at once. NodeLabels returns them in an unspecified order, so callers that need a stable order sort the result.
package main
import (
"fmt"
"sort"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)
func main() {
g := lpg.New[string, int](adjlist.Config{Directed: true})
_ = g.AddNode("alice")
_ = g.SetNodeLabel("alice", "Person")
_ = g.SetNodeLabel("alice", "Employee")
labels := g.NodeLabels("alice")
sort.Strings(labels)
fmt.Println(labels)
}
Output: [Employee Person]
func (*Graph[N, W]) NodeLabelsAsOf ¶ added in v0.11.0
NodeLabelsAsOf is Graph.NodeLabelsByIDAsOf keyed by the external node key.
Safe for concurrent use.
func (*Graph[N, W]) NodeLabelsByID ¶
NodeLabelsByID is the NodeID-keyed counterpart of Graph.NodeLabels. It skips the external-key → NodeID Mapper lookup for callers that already hold the NodeID (the Cypher result-materialisation path), returning the label names in unspecified order, or nil when id carries no labels.
func (*Graph[N, W]) NodeLabelsByIDAsOf ¶ added in v0.11.0
NodeLabelsByIDAsOf returns the names of every label id carried at s, in unspecified order, or nil when it carried none.
Safe for concurrent use.
func (*Graph[N, W]) NodeLabelsInUse ¶ added in v0.5.0
NodeLabelsInUse returns the distinct names of every label currently attached to at least one non-tombstoned node, in unspecified order. Labels borne only by tombstoned (removed) nodes are excluded.
The returned slice is freshly allocated and non-nil; when no live node carries a label it is empty (len 0). The caller owns the slice and may mutate it.
NodeLabelsInUse is safe for concurrent use. It snapshots each of the 16 node-label shards under that shard's RLock (one at a time) and resolves ids through the lock-free LabelRegistry. The result is a point-in-time view and is not guaranteed to be consistent across shards.
func (*Graph[N, W]) NodeLifeVersionCount ¶ added in v0.11.0
NodeLifeVersionCount returns the number of live birth and death records.
Safe for concurrent use.
func (*Graph[N, W]) NodeProperties ¶
func (g *Graph[N, W]) NodeProperties(n N) map[string]PropertyValue
NodeProperties returns a snapshot of every property currently attached to n.
func (*Graph[N, W]) NodePropertiesAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) NodePropertiesAsOf(n N, s *Snapshot) map[string]PropertyValue
NodePropertiesAsOf is Graph.NodePropertiesByIDAsOf keyed by the external node key.
Safe for concurrent use.
func (*Graph[N, W]) NodePropertiesByID ¶
func (g *Graph[N, W]) NodePropertiesByID(id graph.NodeID) map[string]PropertyValue
NodePropertiesByID is the NodeID-keyed counterpart of Graph.NodeProperties. It skips the external-key → NodeID Mapper lookup, so callers that already hold the NodeID — chiefly the Cypher result-materialisation path, which resolves the NodeID once for identity and then needs both properties and labels — avoid a redundant Mapper round-trip per node. The returned map is a fresh copy owned by the caller; it is nil when id has no recorded properties. Concurrency-safe under the same contract as NodeProperties.
func (*Graph[N, W]) NodePropertiesByIDAsOf ¶ added in v0.11.0
NodePropertiesByIDAsOf returns a map of every property id carried at s, or nil when it carried none.
Safe for concurrent use.
func (*Graph[N, W]) NodePropertiesByIDFunc ¶ added in v0.3.1
func (g *Graph[N, W]) NodePropertiesByIDFunc(id graph.NodeID, visit func(name string, pv PropertyValue))
NodePropertiesByIDFunc invokes visit once per property attached to the node identified by id, passing the resolved property name and a value copy of the PropertyValue. It is the allocation-fusing counterpart of Graph.NodePropertiesByID: callers that immediately re-key every property into a different map (chiefly the Cypher result-materialisation path, which converts each lpg.PropertyValue into a cypher/expr value) would otherwise allocate a throwaway intermediate map[string]PropertyValue only to range over it once. Streaming the bag through visit lets the caller build its target map directly, removing that intermediate allocation per returned node.
visit is called zero times for a node with no recorded properties (and for an unknown id). The iteration order is unspecified, matching Go map iteration.
Concurrency and isolation: visit runs while the property shard's read lock is held, so it observes a consistent snapshot of the node's properties relative to any concurrent writer holding the shard write lock — identical to the guarantee of Graph.NodePropertiesByID. visit therefore MUST NOT call back into any Graph method that takes a property-shard lock (it would deadlock) and MUST NOT retain the PropertyValue beyond the callback in a way that aliases graph-internal state; the PropertyValue passed in is a value copy, so copying it out (or deriving an independent value from it) is safe and is the intended use.
func (*Graph[N, W]) NodePropertiesByIDFuncAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) NodePropertiesByIDFuncAsOf(id graph.NodeID, snap *Snapshot, visit func(name string, pv PropertyValue))
NodePropertiesByIDFuncAsOf is Graph.NodePropertiesByIDFunc as the node stood at snap. A nil snapshot reads the current value; see snapshot_read.go.
Safe for concurrent use.
func (*Graph[N, W]) NodePropertyByID ¶ added in v0.3.1
NodePropertyByID returns the single property keyed by name attached to the node identified by id, without materialising the node's full property map. It is the single-key counterpart of Graph.NodePropertiesByID and exists for the Cypher scalar-projection fast path: a predicate or projection that reads only n.name from a bound node fetches just that one value instead of copying every property into a fresh map per row.
The boolean reports whether the property is present (false for both an unknown key name and a node that carries no such property), mirroring the missing-key-is-null semantics of openCypher property access. The returned PropertyValue is a value copy owned by the caller. Concurrency-safe under the same contract as Graph.NodeProperties: the read holds the property shard's read lock for the duration of the lookup, so it observes a consistent view of the node's properties relative to any concurrent writer holding the shard write lock.
func (*Graph[N, W]) NodePropertyByIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) NodePropertyByIDAsOf(id graph.NodeID, key string, s *Snapshot) (PropertyValue, bool)
NodePropertyByIDAsOf returns the value id carried under key at s.
Safe for concurrent use.
func (*Graph[N, W]) OutDegree ¶ added in v0.11.0
OutDegree returns the number of out-neighbours of src that a traversal would visit, without enumerating them. ok is false when src is not interned; a node with no outgoing edges reports (0, true).
It exists so a degree-answerable question — "does this node have any outgoing :KNOWS edge?", "how many does it have?" — costs a counter read rather than an expansion. The audit that motivated it measured `COUNT { (a)-[:K]->(:P) } > 0` at 88× a bare label scan per outer row, because the count was reached by enumerating every neighbour in order to compare against zero.
Live-node semantics ¶
Unlike github.com/FlavioCFOliveira/GoGraph/graph/adjlist.AdjList.OutDegree, which counts adjacency slots, this method excludes edges whose far endpoint has been tombstoned. That is the difference that makes it substitutable for a traversal: Graph.RemoveNode tombstones a node and strips it from the label bitmaps but does NOT remove the incident edges other nodes hold, so a raw slot count would include an edge to a node the query layer treats as absent.
For an UNDIRECTED graph the result is the node's full degree, because the adjacency mirrors insertion. For a DIRECTED graph it is the out-degree only: in-degree is not an adjacency-local quantity and is served by the reverse CSR.
Cost ¶
O(1) when the graph holds no tombstones, which is the common case — the tombstone set is consulted through one lock-free counter, and on zero the adjacency column length is returned directly. O(d) in the node's degree once anything has been deleted, since each far endpoint must then be checked.
Concurrency ¶
Safe for concurrent use with readers and writers, and lock-free.
func (*Graph[N, W]) OutDegreeBoundedByID ¶ added in v0.11.0
OutDegreeBoundedByID is Graph.OutDegreeByID with an early exit: it returns min(trueLiveOutDegree, limit) and stops as soon as limit live out-edges have been counted. A non-positive limit returns 0 without inspecting any edge, the correct answer for "at most zero".
It is the untyped counterpart of Graph.OutDegreeByTypeBoundedByID, and it exists because the untyped degree is only O(1) while the graph is free of tombstones. Once ANY node has been deleted — anywhere in the graph, related or not — the count has to exclude edges landing on a tombstone, which means walking the node's adjacency. Without a limit to stop it, that walk runs to the end of a supernode's column to answer a question a bounded caller settled after one edge: rmp #2265 measured `EXISTS { (a)-->() }` at degree 400 000 going from 2 µs to 2.243 ms — 1170×, permanent and graph-wide — after a single unrelated DELETE, purely because the caller's limit was replaced with maxInt here.
The cap counts LIVE edges only ¶
The limit is charged per edge that SURVIVES the liveness gate, never per slot inspected. That distinction is the whole correctness of the bound: a node whose first slots all point at tombstoned neighbours must keep walking past them, and a cap that counted slots would stop early and report a degree of zero for a node that has live edges further along its column. The gate and the cap are independent concerns, and github.com/FlavioCFOliveira/GoGraph/graph/adjlist.AdjList.OutDegreeFuncBoundedByID keeps them so.
Cost ¶
O(1) when the graph holds no tombstones; O(min(d, limit)) once anything has been deleted, where the min is taken over LIVE edges as described above — a column of tombstoned slots is still walked through.
Concurrency ¶
Safe for concurrent use with readers and writers, and lock-free.
func (*Graph[N, W]) OutDegreeBoundedByIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) OutDegreeBoundedByIDAsOf(srcID graph.NodeID, limit int, snap *Snapshot) (int, bool)
OutDegreeBoundedByIDAsOf is Graph.OutDegreeBoundedByID as the node stood at snap. A nil snapshot reads the current value.
The tombstone gate is NOT yet versioned: a node deleted after this reader started is still excluded. That is the candidate-set gap P4c (rmp #2290) closes; until then the visibility barrier is what keeps a read from straddling it.
Safe for concurrent use.
func (*Graph[N, W]) OutDegreeByID ¶ added in v0.11.0
OutDegreeByID is Graph.OutDegree keyed by an already-resolved graph.NodeID. Tombstoned far endpoints are excluded exactly as Graph.OutDegree excludes them, through the same predicate.
It exists for the query layer, which holds ids and would otherwise pay an id → node-value → id round-trip (an array read plus a string hash) on every call; see github.com/FlavioCFOliveira/GoGraph/graph/adjlist.AdjList.OutDegreeByID for the measurement that motivated it.
Cost ¶
O(1) when the graph holds no tombstones; O(d) once anything has been deleted. A caller that only needs to know whether the degree reaches some bound should use Graph.OutDegreeBoundedByID, which does not pay the full O(d) walk.
Concurrency ¶
Safe for concurrent use with readers and writers, and lock-free.
func (*Graph[N, W]) OutDegreeByType ¶ added in v0.11.0
OutDegreeByType is Graph.OutDegree restricted to edges whose relationship type is relType. ok is false when src is not interned.
Cost ¶
O(d) in the node's degree: the relationship type of each slot must be resolved to decide which edges match. No allocation, and — on a graph with no overflow labels, which is every Cypher-built one — no lock and no access beyond src's own columns and the per-handle records of its typed slots.
It routes through the same walk as Graph.OutDegreeByTypeBoundedByID rather than reading the adjacency label column directly. The column alone is not the whole per-slot truth — a Cypher-created edge records its type against its stable handle — so the column-only count reported 1 for three Cypher-created parallel :K edges where 3 was correct, and 0 for a type that had spilled to the pair's overflow list. Sharing the walk is what makes the bounded and unbounded forms unable to disagree about WHICH edges count, which is the contract their documentation rests on (rmp #2241/#2258).
func (*Graph[N, W]) OutDegreeByTypeBounded ¶ added in v0.11.0
OutDegreeByTypeBounded is Graph.OutDegreeByType capped at limit: it stops as soon as limit matching edges have been counted and returns min(trueDegree, limit). ok is false when src is not interned.
It answers a comparison of a typed degree against a small literal without walking a high-degree node to the end. The untyped degree has no bounded companion because it is already O(1) — there is nothing to stop early.
Tombstoned far endpoints are excluded exactly as Graph.OutDegreeByType excludes them, through the same predicate, so a bounded and an unbounded count can never disagree about WHICH edges count — only about when they stop counting.
Cost ¶
O(min(d, limit)) in the node's degree. No allocation.
Concurrency ¶
Safe for concurrent use with readers and writers, and lock-free.
func (*Graph[N, W]) OutDegreeByTypeBoundedByID ¶ added in v0.11.0
func (g *Graph[N, W]) OutDegreeByTypeBoundedByID(srcID graph.NodeID, relType LabelID, limit int) (int, bool)
OutDegreeByTypeBoundedByID is Graph.OutDegreeByTypeBounded keyed by an already-resolved graph.NodeID. See Graph.OutDegreeByID.
func (*Graph[N, W]) OutDegreeByTypeBoundedByIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) OutDegreeByTypeBoundedByIDAsOf(srcID graph.NodeID, relType LabelID, limit int, snap *Snapshot) (int, bool)
OutDegreeByTypeBoundedByIDAsOf is Graph.OutDegreeByTypeBoundedByID as the node stood at snap. A nil snapshot reads the current value.
Safe for concurrent use.
func (*Graph[N, W]) OutDegreeMatchingBoundedByID ¶ added in v0.11.0
func (g *Graph[N, W]) OutDegreeMatchingBoundedByID( srcID graph.NodeID, relType LabelID, typed bool, limit int, farOK func(dst graph.NodeID) bool, ) (int, bool)
OutDegreeMatchingBoundedByID counts srcID's out-edges whose far endpoint satisfies farOK, optionally restricted to one relationship type, capped at limit. It returns min(trueMatchingCount, limit); ok is false when srcID is not interned.
When typed is false relType is ignored and every out-edge is offered to farOK.
It exists for a caller that must qualify the FAR NODE — the cypher engine counting `(a)-[:K]->(:P)` without materialising a neighbour (rmp #2235). A plain degree cannot answer that: a degree counts every out-edge and has no way to ask anything about where the edge lands. Pushing the predicate in here rather than exposing the raw adjacency walk keeps the TOMBSTONE GATE in one place — a caller that assembled this from [AdjList.OutDegreeFuncBoundedByID] would have to re-derive the liveness rule and could drift from Graph.OutDegreeByType, which is precisely the disagreement about WHICH edges count that the bounded/unbounded pair is documented to rule out.
farOK is called at most once per out-edge, in adjacency order, and only for edges that already passed the type and liveness gates — so it never sees a tombstoned endpoint and need not re-check one.
Cost ¶
O(min(d, limit)) in the node's degree, plus the cost of farOK. No allocation.
Concurrency ¶
Safe for concurrent use with readers and writers, and lock-free, on the same terms as Graph.OutDegreeByTypeBoundedByID.
func (*Graph[N, W]) OutDegreeMatchingBoundedByIDAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) OutDegreeMatchingBoundedByIDAsOf( srcID graph.NodeID, relType LabelID, typed bool, limit int, farOK func(dst graph.NodeID) bool, snap *Snapshot, ) (int, bool)
OutDegreeMatchingBoundedByIDAsOf is Graph.OutDegreeMatchingBoundedByID as the node stood at snap. A nil snapshot reads the current value.
Safe for concurrent use.
func (*Graph[N, W]) PropDeltaCount ¶ added in v0.11.0
PropDeltaCount returns the number of live node-property delta records.
The lock-free gate a property reader consults, and the memory bound the design owes a garbage-collection phase.
Safe for concurrent use.
func (*Graph[N, W]) PropertyKeys ¶
func (g *Graph[N, W]) PropertyKeys() *PropertyKeyRegistry
PropertyKeys returns the property-key registry.
func (*Graph[N, W]) PropertyKeysInUse ¶ added in v0.5.0
PropertyKeysInUse returns the distinct names of every property key present on at least one non-tombstoned node, or on at least one edge whose endpoints are both non-tombstoned, in unspecified order. The result is the union across the node and edge property stores.
The returned slice is freshly allocated and non-nil; when no live element carries a property it is empty (len 0). The caller owns the slice and may mutate it.
PropertyKeysInUse is safe for concurrent use. It snapshots each of the 16 node-property shards and each of the 16 edge-property shards under that shard's RLock (one at a time) and resolves ids through the lock-free PropertyKeyRegistry. The result is a point-in-time view and is not guaranteed to be consistent across shards.
func (*Graph[N, W]) ReadAt ¶ added in v0.11.0
ReadAt binds g to snap, returning a view whose every read is resolved at that instant. A nil snapshot yields a view that reads the current stored value.
It allocates: one small header per query, against a query that already allocates a physical operator tree. The alternative — a value type — would be copied at every call site that stores it in a struct.
func (*Graph[N, W]) ReclaimNow ¶ added in v0.11.0
ReclaimNow frees every version no active reader can reach, synchronously, and returns how many records were released.
The watermark is the oldest start timestamp among the readers registered with this graph's horizon, falling back to the clock's current value when none is registered — at which point every version a completed write superseded is unreachable and all of it goes. A reader that could not be registered suspends reclamation entirely rather than risk it; see mvcc.Horizon.Oldest.
The background vacuum is what keeps memory bounded in the ordinary case (mvcc_vacuum.go). This is exported for the two things the vacuum cannot give a caller: a bulk loader that knows it has just finished and wants the debt settled at a known instant rather than a few milliseconds later, and a test that needs the substrate's state to be a function of what it did rather than of when a goroutine woke.
Unlike the vacuum's own pass it is UNBOUNDED — it sweeps every store to completion — which is what makes it a settlement rather than a step.
What the caller must exclude: nothing ¶
Concurrent WRITERS are excluded by each reclaimer's own per-shard lock; see [Graph.sweepUnit] for the body-by-body verification. Concurrent SWEEPS are excluded by this method itself — it takes the same single-sweeper slot the vacuum's pass takes, so the two can never walk the same chain, and the wait for it is bounded because a vacuum pass is bounded ([vacuumRecordsPerPass]).
Safe for concurrent use.
func (*Graph[N, W]) ReclaimVersions ¶ added in v0.11.0
ReclaimVersions frees every VERSION CHAIN record that no reader can reach any more — node labels, node properties and the per-edge side stores — and returns how many were released.
Node EXISTENCE records are deliberately NOT included: they are a pair of instants rather than a value history, they are swept by Graph.ReclaimNow alongside the adjacency, and folding them in here would make this function's count mean two different things.
watermark is the oldest start timestamp among active readers, from mvcc.Horizon.Oldest. Zero means reclaim nothing, which is what the horizon reports while a reader could not be registered — the sound answer when the oldest reader is unknown.
Safe for concurrent use with readers. Not safe to run concurrently with itself.
func (*Graph[N, W]) Registry ¶
func (g *Graph[N, W]) Registry() *LabelRegistry
Registry returns the underlying label registry.
func (*Graph[N, W]) RelationshipTypesInUse ¶ added in v0.5.0
RelationshipTypesInUse returns the distinct names of every edge label attached to at least one edge whose endpoints are both non-tombstoned, in unspecified order. An edge label survives only while at least one live edge (both endpoints live) still bears it.
The returned slice is freshly allocated and non-nil; when no live edge carries a label it is empty (len 0). The caller owns the slice and may mutate it.
RelationshipTypesInUse is safe for concurrent use. It walks the inline per-slot label column of every source's adjacency (the lock-free snapshot) and each of the 16 edge-label overflow shards under that shard's RLock (one at a time), and resolves ids through the lock-free LabelRegistry. The result is a point-in-time view and is not guaranteed to be consistent across shards.
func (*Graph[N, W]) RemoveAllEdgesFrom ¶ added in v0.3.0
func (g *Graph[N, W]) RemoveAllEdgesFrom(src N)
RemoveAllEdgesFrom removes all edges incident from src in O(d) time for a degree-d hub, rather than the O(d²) cost of d sequential Graph.RemoveEdge calls. After clearing the adjacency layer it also clears the per-pair edge state (labels, properties, handles, instance records, CREATE counters) for every endpoint pair that src was involved in, exactly as Graph.RemoveEdge does for each individual edge.
For directed graphs the outgoing edges are removed and their forward per-pair state is cleared. For undirected graphs the mirror entries are also removed and both directions' per-pair state are cleared.
RemoveAllEdgesFrom is safe for concurrent use.
func (*Graph[N, W]) RemoveEdge ¶
func (g *Graph[N, W]) RemoveEdge(src, dst N)
RemoveEdge removes one edge (src, dst) from the adjacency layer (and the mirrored (dst, src) edge when the graph is undirected). When this leaves the endpoint pair with NO remaining edge — the last parallel edge between them is gone — RemoveEdge also strips the per-pair edge labels and edge properties, so re-creating an edge between the same endpoints later does not resurrect the removed edge's labels or properties (the edge analogue of node-tombstone hygiene). While any parallel edge between the pair survives, the shared per-pair label and property surfaces are left intact.
RemoveEdge is the edge-deletion entry point used by the Cypher executor and WAL replay, so the in-memory state and the recovered state agree. Callers that operate purely on adjacency (e.g. search algorithms) may keep using adjlist.AdjList.RemoveEdge directly; that path does not touch labels or properties.
Example ¶
ExampleGraph_RemoveEdge shows that deleting an edge clears its per-pair label/property surface once the endpoint pair is fully disconnected, so re-creating an edge between the same endpoints does not resurrect the removed relationship's type.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)
func main() {
g := lpg.New[string, int](adjlist.Config{Directed: true})
_ = g.AddEdge("alice", "bob", 0)
g.SetEdgeLabel("alice", "bob", "KNOWS")
fmt.Println("before delete:", g.HasEdgeLabel("alice", "bob", "KNOWS"))
g.RemoveEdge("alice", "bob")
_ = g.AddEdge("alice", "bob", 0) // re-create the same pair
fmt.Println("after re-create:", g.HasEdgeLabel("alice", "bob", "KNOWS"))
}
Output: before delete: true after re-create: false
func (*Graph[N, W]) RemoveEdgeByHandle ¶ added in v0.9.0
RemoveEdgeByHandle removes the single parallel edge instance identified by the stable handle on the (src, dst) pair — its adjacency slot (via adjlist.AdjList.RemoveEdgeByHandle) AND its per-handle label/property metadata (via Graph.RemoveEdgeInstanceByHandle) — leaving every sibling instance's slot, handle, and metadata intact. It returns true when a slot carrying handle was removed and false when none matched (already removed, wrong handle, or unknown endpoint).
It is the instance-precise analogue of Graph.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 instance, not the lowest-indexed one (rmp #2018). Like RemoveEdge it applies edge tombstone hygiene on the per-pair coalesced surfaces: the shared per-pair labels and properties are captured before the adjacency removal and re-asserted onto the survivors when a sibling remains, and cleared when the removal leaves the pair fully disconnected (so a later re-add between the same endpoints does not resurrect stale labels/properties).
A handle of 0 has no stable identity, so it falls back to Graph.RemoveEdge (first-match) and returns whether an edge was present — a caller that lost the handle still removes one edge rather than silently no-opping.
RemoveEdgeByHandle is the by-handle edge-deletion entry point used by the Cypher executor and WAL replay, so the in-memory state and the recovered state agree.
func (*Graph[N, W]) RemoveEdgeInstance ¶
RemoveEdgeInstance discards every per-instance label and property for (src, dst) at `idx` so subsequent reads (EdgeLabelsAt / EdgePropertiesAt) return empty. Used by DELETE to drop a specific logical edge while leaving sibling instances at other indices untouched.
RemoveEdgeInstance is safe for concurrent use.
func (*Graph[N, W]) RemoveEdgeInstanceByHandle ¶
RemoveEdgeInstanceByHandle discards every per-handle label and property for (src, dst) at handle so subsequent reads (EdgeLabelsByHandle / EdgePropertiesByHandle) return empty. The handle-keyed analogue of Graph.RemoveEdgeInstance; used by DELETE to drop one logical edge while leaving sibling handles untouched. No-op when handle is 0.
RemoveEdgeInstanceByHandle is safe for concurrent use.
func (*Graph[N, W]) RemoveEdgeLabel ¶ added in v0.2.0
RemoveEdgeLabel detaches name from the directed edge (src, dst). It is the exported inverse of Graph.SetEdgeLabel used by the Cypher executor's transaction-undo path to strip a label a failed write query had attached. No-op when either endpoint is unknown, name was never interned, or the label is not present on the pair. Unlike Graph.SetEdgeLabel it does not require the edge to still exist in the adjacency, so it can also undo a label that was set on an edge later removed within the same failed statement.
Like [Graph.clearEdgePairState], the coarse src-keyed edge label index (g.edgeIdx) is intentionally left untouched: it is read only as an over-approximation the executor verifies against the authoritative per-pair labels, so a stale entry can cost at most a filtered-out candidate, never a wrong result.
RemoveEdgeLabel is safe for concurrent use.
func (*Graph[N, W]) RemoveNode ¶
func (g *Graph[N, W]) RemoveNode(n N)
RemoveNode marks the node n as removed. Subsequent reads through IsTombstoned / LiveOrder / TombstonedIDs treat n as absent. The underlying Mapper retains the slot (NodeID stability is a hard contract), but label, property, and adjacency reads on the tombstoned id remain safe; callers should also strip labels / properties / incident edges before calling RemoveNode so the tombstone reflects the fully-deleted node state. No-op when n was never interned or is already tombstoned.
Example ¶
ExampleGraph_RemoveNode shows that node deletion is a tombstone — the NodeID slot is permanent, so the node is excluded from the live count rather than reusing its id — and that re-creating the same key revives the node under the SAME stable NodeID. This is what makes a delete-then-recreate cycle yield exactly one live node, and (once the tombstone set is persisted) survive a store reopen.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)
func main() {
g := lpg.New[string, int](adjlist.Config{Directed: true})
_ = g.SetNodeLabel("auth", "Spec")
id, _ := g.AdjList().Mapper().Lookup("auth")
g.RemoveNode("auth")
fmt.Println("tombstoned:", g.IsTombstoned(id), "live:", g.LiveOrder())
// Re-create the same key: revived under the same NodeID.
_ = g.AddNode("auth")
id2, _ := g.AdjList().Mapper().Lookup("auth")
fmt.Println("revived:", !g.IsTombstoned(id), "sameID:", id == id2, "live:", g.LiveOrder())
}
Output: tombstoned: true live: 0 revived: true sameID: true live: 1
func (*Graph[N, W]) RemoveNodeLabel ¶
RemoveNodeLabel detaches name from n. No-op if absent.
func (*Graph[N, W]) RemoveStoreConstraint ¶ added in v0.6.0
RemoveStoreConstraint drops the store-direct constraint slot identified by (kind, label, property), the dual of Graph.AddStoreConstraint for a committed OpDropConstraint. Dropping a constraint that was never recorded is a no-op, so a DROP that suppresses a CREATE folded away by a prior checkpoint cannot drive the active count negative.
RemoveStoreConstraint is safe for concurrent use.
func (*Graph[N, W]) RemoveStoreIndex ¶ added in v0.6.0
RemoveStoreIndex drops the store-direct index slot identified by name, the dual of Graph.AddStoreIndex for a committed OpDropIndex. Dropping an index that was never recorded is a no-op, so a DROP that suppresses a CREATE folded away by a prior checkpoint cannot drive the active count negative.
RemoveStoreIndex is safe for concurrent use.
func (*Graph[N, W]) RestoreMVCCClock ¶ added in v0.11.0
RestoreMVCCClock raises this graph's MVCC clock so every instant it subsequently allocates, and every instant a new reader starts at, is at or above floor. It never lowers the clock.
It is a no-op when the versioning substrate is disarmed, so a recovery path may call it unconditionally.
Why the clock is restored at all, and why by derivation (rmp #2309) ¶
mvcc.Clock is process-local and constructed at zero on every open. Nothing persists it, deliberately: two of the three reference engines removed their persisted counter and derive instead — InnoDB folds a max over the rollback segments at startup, Memgraph derives max(delta_ts)+1 from the WAL. A second durable source of truth is one that can disagree with the log after a torn tail.
So recovery reads the largest commit timestamp the WAL actually carries and hands it here as a floor. Without it a reopened graph re-mints instants a previous process already published and made durable, and a reader could reach a version that is simultaneously in its past and its future.
It must be called before the graph has readers or writers ¶
It moves the visible frontier as well as the allocation counter, which is sound only because recovery has no commits in flight: every transaction in the file either reached its durable marker or went with the torn tail. See mvcc.Clock.RatchetTo.
Not safe for concurrent use.
func (*Graph[N, W]) RestoreTombstones ¶
RestoreTombstones marks every id in ids as removed, reconstructing the tombstone set captured by Graph.TombstonedIDs at snapshot time. It is the load-phase dual of Graph.RemoveNode used by snapshot recovery: it re-tombstones by NodeID directly and does not require the natural key to be resolvable. A later Graph.AddNode for the same id still revives it, so a delete→recreate that straddles a snapshot resolves correctly.
RestoreTombstones is intended for the one-shot snapshot-load phase of recovery and is not safe to call concurrently with other mutations or reads on g.
func (*Graph[N, W]) Revive ¶ added in v0.2.0
func (g *Graph[N, W]) Revive(n N)
Revive clears any tombstone on the node interned under key n, marking it live again. It is the exported, key-addressed inverse of Graph.RemoveNode used by the Cypher executor's transaction-undo path to restore a node that a failed write query had tombstoned. No-op when n was never interned or is not currently tombstoned. The clear is taken under the same lock as Graph.IsTombstoned/Graph.LiveOrder, so it is atomic against those readers.
Revive is safe for concurrent use.
func (*Graph[N, W]) SeedEdgeHandle ¶
SeedEdgeHandle raises the per-graph stable-handle high-water counter so the next Graph.AddEdgeH returns a value strictly greater than `next-1` — i.e. at least `next`. It is called once at the end of recovery with max(live handle)+1 so a post-recovery edge creation never re-mints a handle that is already live on disk (invariant I5: handles stay unique and monotone across a reopen).
The operation is monotone: seeding with a value at or below the current counter is a no-op, so calling it with a stale `next` cannot rewind the counter. SeedEdgeHandle is safe for concurrent use, though recovery calls it from the single load goroutine.
func (*Graph[N, W]) SetConstraintCountSource ¶ added in v0.11.0
SetConstraintCountSource attaches the function Graph.HasConstraints derives the engine's schema-constraint count from. Called once, at wiring time, by whatever owns the constraint registry; a nil src detaches it.
It replaces a SetActiveConstraintCount(n int64) that stored a count the caller had read separately. That shape is a lost update as soon as a second writer exists — A reads 1, B reads 2 and stores 2, A stores 1, and the gate under-reports, which makes the checkpointer truncate the WAL prefix holding a CREATE CONSTRAINT (#1464). Deriving removes the window rather than guarding it: there is no stored value to go stale, so the gate needs no ordering guarantee from its caller at all. See the field comment on constraintCount.
src must be safe for concurrent use: it is called from readers, including the checkpointer, with no lock held here.
func (*Graph[N, W]) SetEdgeLabel ¶
SetEdgeLabel attaches label to the directed edge (src, dst). The edge must already exist in the underlying adjacency list; otherwise the call is a no-op. The label is associated with the source NodeID's row in the edge index.
The first relationship type of a pair is stored inline in the adjacency slot's label column; a second distinct type spills to the per-shard overflow store. The two together form the pair's derived label set returned by Graph.EdgeLabels. The whole update runs under the pair's edge-label shard write lock so the slot and overflow halves transition together with respect to a concurrent reader.
func (*Graph[N, W]) SetEdgeLabelAt ¶
SetEdgeLabelAt attaches `name` to the directed edge instance (src, dst) at the supplied 1-based CREATE index. No-op when either endpoint is unknown to the underlying mapper.
SetEdgeLabelAt is safe for concurrent use.
func (*Graph[N, W]) SetEdgeLabelByHandle ¶
SetEdgeLabelByHandle attaches name to the directed edge identified by the stable handle on the (src, dst) pair. No-op when handle is 0 (the no-handle sentinel) or when either endpoint is unknown to the mapper.
SetEdgeLabelByHandle is safe for concurrent use.
func (*Graph[N, W]) SetEdgeLabelByHandleID ¶
SetEdgeLabelByHandleID attaches `name` to the edge identified by `handle` on the directed (srcID, dstID) NodeID pair, resolving by NodeID rather than natural key. It is the NodeID-keyed dual of Graph.SetEdgeLabelByHandle used by the snapshot/WAL recovery path, which has already restored the mapper by NodeID and must not pay a Resolve→Lookup round trip. No-op when handle is 0.
SetEdgeLabelByHandleID is safe for concurrent use.
func (*Graph[N, W]) SetEdgeProperty ¶
func (g *Graph[N, W]) SetEdgeProperty(src, dst N, key string, value PropertyValue) error
SetEdgeProperty records the named property on the directed edge (src, dst). The edge must already exist; otherwise the call is a no-op (mirroring SetEdgeLabel). Returns any error returned by the installed SchemaValidator; when the validator rejects the write the graph state is left unchanged.
The value is written into the per-slot columnar block of src at every slot whose neighbour is dst, so the per-pair view coalesces to the latest value for the key. The write is copy-on-write under the adjacency shard lock: a new immutable column block is built with every dst-matching slot updated and is published with a single atomic store, so a concurrent lock-free reader observes either the prior block or the fully-updated one.
func (*Graph[N, W]) SetEdgePropertyAt ¶
func (g *Graph[N, W]) SetEdgePropertyAt(src, dst N, idx int64, key string, value PropertyValue) error
SetEdgePropertyAt records the property `key`=`value` for the directed edge instance (src, dst) at the supplied 1-based CREATE index. Returns any error returned by the installed SchemaValidator; when the validator rejects the write the graph state is left unchanged.
SetEdgePropertyAt is safe for concurrent use.
func (*Graph[N, W]) SetEdgePropertyByHandle ¶
func (g *Graph[N, W]) SetEdgePropertyByHandle(src, dst N, handle uint64, key string, value PropertyValue) error
SetEdgePropertyByHandle records key=value for the edge identified by handle on the (src, dst) pair. No-op when handle is 0 or when either endpoint is unknown to the mapper. Returns any error returned by the installed SchemaValidator; when the validator rejects the write the graph state is left unchanged.
SetEdgePropertyByHandle is safe for concurrent use.
func (*Graph[N, W]) SetEdgePropertyByHandleID ¶
func (g *Graph[N, W]) SetEdgePropertyByHandleID(srcID, dstID graph.NodeID, handle uint64, key string, value PropertyValue)
SetEdgePropertyByHandleID records key=value on the edge identified by `handle` on the directed (srcID, dstID) NodeID pair. It is the NodeID-keyed dual of Graph.SetEdgePropertyByHandle used by the snapshot/WAL recovery path. No-op when handle is 0.
This method is intentionally called only by the snapshot/WAL recovery path and bypasses the SchemaValidator: values replayed here were validated at the time of the original write and must not fail during recovery.
SetEdgePropertyByHandleID is safe for concurrent use.
func (*Graph[N, W]) SetEdgeRelTypeAtSlotByID ¶ added in v0.11.0
func (g *Graph[N, W]) SetEdgeRelTypeAtSlotByID(srcID, dstID graph.NodeID, ordinal int, name string) bool
SetEdgeRelTypeAtSlotByID attaches name as a relationship type to the ONE slot of the directed pair (srcID → dstID) sitting at the supplied canonical ordinal (see the file comment). It reports whether the ordinal resolved to a slot; a pair holding fewer slots than the ordinal demands is a no-op returning false, which is how the snapshot apply path degrades when the adjacency it replays onto is not the one the records were written from.
It is the per-SLOT counterpart of Graph.SetEdgeLabel, which names the PAIR and therefore types every free column-typed slot of it. The type goes into the slot's own label column when that column is free. When the column already holds a DIFFERENT type the call does NOT overwrite it — a slot carries {inline} plus the pair's overflow, so the extra type spills to the overflow, exactly where SetEdgeLabel puts a type it cannot place per slot. Not overwriting is what keeps a snapshot replayed on top of a WAL tail from destroying the tail's more recent type. Re-asserting a type the slot already holds inline is a no-op that still reports true.
It does NOT write the by-handle type store, which has its own durable component and stays authoritative for the slots it covers; this writes the adjacency label column, which is what Graph.EdgeLabels, Graph.HasEdgeLabel and Graph.RelationshipTypesInUse read regardless of whether a handle record also exists.
SetEdgeRelTypeAtSlotByID is safe for concurrent use.
func (*Graph[N, W]) SetIndexCountSource ¶ added in v0.11.0
SetIndexCountSource attaches the function Graph.HasIndexes derives the engine's secondary-index count from — the index analogue of Graph.SetConstraintCountSource (#1755), derived for the same reason. Called once at wiring time; a nil src detaches it.
src must be safe for concurrent use: it is called from readers, including the checkpointer's phase-3 re-check, with no lock held here.
func (*Graph[N, W]) SetIndexManager ¶
SetIndexManager installs m as the manager of secondary indexes on this graph. Passing nil detaches the current manager. The Graph retains a borrowed reference to m; the caller owns m's lifetime.
SetIndexManager is safe for concurrent use; the pointer is stored with sequential consistency. Goroutines that call Graph.IndexManager after this store returns will observe m (or a later value).
func (*Graph[N, W]) SetNodeLabel ¶
SetNodeLabel attaches label to n, inserting n if needed. Returns the error from the underlying adjlist.AdjList.AddNode (which can only happen via a future bounded-growth implementation); the current adjlist.AdjList.AddNode never fails, so callers in codepaths that do not configure adjlist.Config.MaxShardCapacity may safely ignore the return.
func (*Graph[N, W]) SetNodeProperty ¶
func (g *Graph[N, W]) SetNodeProperty(n N, key string, value PropertyValue) error
SetNodeProperty records the named property on n with the given value, inserting n into the graph if necessary. Returns the error from the underlying adjlist.AdjList.AddNode when present, or any error returned by the installed SchemaValidator.
func (*Graph[N, W]) SetValidator ¶
func (g *Graph[N, W]) SetValidator(v SchemaValidator)
SetValidator installs v as the runtime schema validator for this graph. Once set, every call to Graph.SetNodeProperty and Graph.SetEdgeProperty will invoke v.Validate before applying the write; a non-nil error from Validate causes the write to be rejected and the error returned to the caller.
When v also implements NodeValidator (as *schema.Schema does), whole-node invariants such as required-property existence are enforced separately, at the node-finalisation boundary, via Graph.ValidateNode. Per-property typing is enforced eagerly here at each Graph.SetNodeProperty; existence cannot be, because a node acquires its properties one mutation at a time and is not complete until finalised.
Pass nil to remove any previously installed validator.
SetValidator is safe for concurrent use.
func (*Graph[N, W]) SideEffectCounters ¶
func (g *Graph[N, W]) SideEffectCounters() (nodesAdded, nodesRemoved, edgesAdded, edgesRemoved uint64)
SideEffectCounters returns the per-direction counters maintained by the graph: nodes added, nodes removed, edges added, edges removed since SnapshotSideEffectCounters was last called. Used by the Cypher TCK side-effect comparator to verify +nodes / -nodes / +relationships / -relationships are accurate counts (not net changes).
func (*Graph[N, W]) StoreConstraints ¶ added in v0.8.0
func (g *Graph[N, W]) StoreConstraints() []StoreConstraint
StoreConstraints returns a snapshot of the store-direct schema constraints recorded on this graph (seeded by recovery, or by the txn.Store apply path). It lets the cypher engine re-enforce durable UNIQUE / NOT NULL constraints when it opens over a recovered store even if the caller did not thread them explicitly (see cypher.NewEngineWithStore). The returned order is unspecified; the slice is a fresh copy the caller owns.
StoreConstraints is safe for concurrent use.
func (*Graph[N, W]) TombstoneCount ¶
TombstoneCount returns the number of NodeIDs currently marked removed. It reads a lock-free counter, so it is cheap enough to gate the optional emission of the snapshot tombstone component on every checkpoint.
TombstoneCount is safe for concurrent use.
func (*Graph[N, W]) TombstonedIDs ¶
TombstonedIDs returns the NodeIDs currently marked removed via Graph.RemoveNode, in ascending order. The result is a fresh slice the caller owns; an empty (never-deleted) graph returns a zero-length slice. Used by the snapshot writer to persist the tombstone set durably so node deletions survive a store reopen.
TombstonedIDs is safe for concurrent use: it loads the immutable published bitmap once and reads it without any lock.
func (*Graph[N, W]) TombstonedIDsAsOf ¶ added in v0.11.0
TombstonedIDsAsOf returns, in ascending order, every interned node id that did NOT exist as of s — the set a snapshot must record so a recovered graph has the same live nodes the image was taken from (rmp #2310).
A nil snapshot returns Graph.TombstonedIDs, the present-time answer.
Why it does not read the tombstone bitmap ¶
The bitmap is a COW accelerator maintained beside the versioned truth, and it answers "is this node removed NOW". A capture that no longer excludes writers needs "was this node removed as of s", and those differ by exactly the transactions that committed during the capture — which is the whole class of partial-transaction image this task exists to make impossible. So the authoritative store is consulted: Graph.NodeExistsAsOf, which resolves the node's birth and death records against s.
The cost is one existence test per interned id, O(V). A capture already walks every node to serialise its labels, properties and adjacency, so this adds a constant factor to a walk that must happen anyway rather than a new traversal — and the present-time form's bitmap read is not available at an arbitrary instant at any price, because the bitmap keeps no history.
Safe for concurrent use.
func (*Graph[N, W]) TopoGeneration ¶ added in v0.7.0
TopoGeneration returns the current value of the graph's edge-topology generation counter (rmp #1871): a purely monotonic count of every change to the graph's LIVE edge topology since it was created. Two reads returning the same value guarantee that topology did not change in between, which is exactly the invalidation signal a CSR-position-keyed cache needs — the Cypher engine's edge-type-filter cache and its forward/reverse CSR pair cache both key on it.
It counts edge additions and removals, undos of either, and — since rmp #2143 — the three TOMBSTONE transitions (RemoveNode, reviving a removed node via AddNode, and RestoreTombstones). Tombstoning touches no edge, but csr.BuildFromAdjListLive omits the arcs incident to a tombstoned node, so the live topology a cache is derived from has changed.
Since rmp #2255 it ALSO counts every change to an edge's derived LABEL set — Graph.SetEdgeLabel, Graph.RemoveEdgeLabel and Graph.SetEdgeLabelByHandle. An edge label moves no CSR position, so this is wider than the counter's name suggests, and the reason is concrete: the Cypher engine's edge-type-filter cache keys on this epoch while resolving relationship TYPE from those labels, so a label change that left the epoch still was served a stale filter. The observable defect was a durably committed relationship-type change staying invisible to a warm Engine indefinitely, and — via the rollback inverse — an aborted one staying visible.
A mutation that changes nothing does NOT bump: re-asserting a label already present, removing one that is absent, or targeting an edge that does not exist all leave the epoch alone. That distinction is load-bearing rather than tidy, because the MERGE MATCH branch re-asserts an existing relationship's type on every match, and bumping there would force an O(V+E) CSR-pair rebuild per MERGE on a read-mostly workload.
It says nothing about interning a fresh node or about property-only mutations, neither of which shifts an existing edge's CSR position or changes a relationship type; see the topoGeneration field doc for why that scope is sufficient and intentional.
Callers do NOT need to bump it themselves: every Graph mutator that changes live topology bumps it internally, after publishing the change. Safe for concurrent use.
func (*Graph[N, W]) UnlockBarrier ¶ added in v0.3.0
func (g *Graph[N, W]) UnlockBarrier()
UnlockBarrier releases the transaction-visibility write lock acquired via Graph.LockBarrier. It MUST be called from the same goroutine that called LockBarrier, and exactly once per LockBarrier call. After this call completes, Graph.ApplyAtomically may be called again from any goroutine. (It used to say "concurrent Graph.View readers may proceed" as well; rmp #2344 removed that reader, and a snapshot reader never waited on this barrier in the first place.)
func (*Graph[N, W]) VacuumStats ¶ added in v0.11.0
func (g *Graph[N, W]) VacuumStats() VacuumStats
VacuumStats returns the current state of the background vacuum.
Safe for concurrent use.
func (*Graph[N, W]) ValidateNode ¶ added in v0.2.0
ValidateNode enforces the installed validator's whole-node invariants against the current, complete label and property set of the node interned under n. It is the node-finalisation hook: a caller building a node (one Graph.AddNode, then any number of Graph.SetNodeLabel and Graph.SetNodeProperty calls) invokes ValidateNode once the node is fully populated to reject it when it violates a required-property/existence constraint that the per-value Graph.SetNodeProperty check cannot detect.
Enforcement is deliberately split from the mutation point. Per-property typing is checked eagerly inside Graph.SetNodeProperty because a single value can be judged in isolation; required-property existence cannot, since a legitimate node receives its label before the property that the label requires (for example CREATE (:User {email:'a@b'}) sets the User label before the email property). Validating existence at the mutation point would reject such a node mid-construction, so existence is enforced here instead, once the node is finalised.
ValidateNode returns nil when no validator is installed, when the installed validator does not implement NodeValidator, or when the node satisfies every whole-node invariant. It does not mutate the graph; on a non-nil return the caller is responsible for rolling back or discarding the half-built node.
ValidateNode is safe for concurrent use, under the same per-operation snapshot contract as Graph.NodeLabels and Graph.NodeProperties: it reads a consistent label set and a consistent property bag, but a writer mutating the same node concurrently may change the node between the two reads. Build a node to completion before finalising it.
func (*Graph[N, W]) VersionCount ¶ added in v0.11.0
VersionCount returns the total number of live version records across every store: node-label deltas, node-property deltas, adjacency entry versions, and the five per-edge side stores (rmp #2291).
It is the memory the substrate is responsible for, and the quantity the bounded-resources mandate requires be observable rather than merely bounded.
Safe for concurrent use.
func (*Graph[N, W]) WalkEdgeHandles ¶
func (g *Graph[N, W]) WalkEdgeHandles(fn func(EdgeHandleTriple) bool)
WalkEdgeHandles calls fn once for every live directed edge slot that carries a non-zero stable handle. It returns early if fn returns false. Slots with a 0 handle (the no-handle sentinel, e.g. a simple-graph edge or a pre-Stage-2 edge) are skipped: there is no durable identity to persist for them.
The walk is the snapshot writer's enumeration of the adjacency handle column. It iterates source nodes in the underlying mapper's [Walk] order — the exact order the CSR, labels and properties snapshot writers use — and within each source in adjacency slot order (insertion order). That makes the persisted edgehandles.bin component byte-stable across writes of the same logical state and aligned with the CSR component, honouring the cross-process byte-equality contract the snapshot relies on.
WalkEdgeHandles is NOT safe for concurrent use with mutations on g.
func (*Graph[N, W]) WalkEdgeHandlesAsOf ¶ added in v0.11.0
func (g *Graph[N, W]) WalkEdgeHandlesAsOf(s *Snapshot, fn func(EdgeHandleTriple) bool)
WalkEdgeHandlesAsOf is Graph.WalkEdgeHandles resolving every node's adjacency AS OF s rather than as of the present (rmp #2310).
A nil snapshot walks the current entries, so it is exactly Graph.WalkEdgeHandles and a caller may pass whatever it has.
It exists for the checkpoint capture, which must read every structure at ONE transactional instant while writers keep committing. The unversioned form reads each node's LIVE entry, so a capture using it would fold the handles of a transaction that committed part-way through the walk — the partial-transaction image that the exclusion this task removes used to prevent.
Nodes are visited in mapper order, which is the order the unversioned form uses; a node that did not exist at s simply has no entry as of s and contributes nothing, so existence needs no separate test here.
Safe for concurrent use.
func (*Graph[N, W]) Writer ¶ added in v0.11.0
Writer returns g's write surface bound to transaction tx.
It allocates nothing.
func (*Graph[N, W]) WriterView ¶ added in v0.11.0
WriterView is [Graph.writerView] for a caller in another package — the Cypher engine's write path, which must read as of the writing transaction rather than as of the present.
It resolves the transaction through the graph's slot, so it answers with whichever write bracket published LAST. That is the caller's own only while at most one bracket is open at a time; prefer Graph.WriterViewOf, which cannot be wrong. This form is kept for the explicit-transaction path, which holds the barrier exclusively and therefore is the only open bracket by construction.
Safe for concurrent use; the returned view is immutable.
func (*Graph[N, W]) WriterViewOf ¶ added in v0.11.0
WriterViewOf returns the graph as write transaction tx reads it: as of the instant tx began, plus the versions tx has written itself.
This is the form the ordinary write path must use. The snapshot comes from the transaction the caller was HANDED rather than from the graph's slot, so a concurrent writer that opened its own bracket in between cannot substitute its snapshot for this one — which is the whole difference rmp #2304 turns on, and the same lesson rmp #2301 learned one level down: reading the writer's identity off the graph produced a FALSE conflict between goroutines writing disjoint nodes (see graph/lpg/mvcc_writectx.go).
A zero tx reads the present, which is the correct answer outside a transaction.
Safe for concurrent use; the returned view is immutable.
type LabelID ¶
type LabelID uint32
LabelID is the compact internal identifier produced by the LabelRegistry for an interned label string.
type LabelRegistry ¶
type LabelRegistry struct {
// contains filtered or unexported fields
}
LabelRegistry interns label names and assigns sequential LabelIDs. It is safe for concurrent use.
Both read paths are fully lock-free: LabelRegistry.Lookup (name→id) loads the immutable forward table through an atomic.Pointer and LabelRegistry.Resolve (id→name) loads the immutable id→name snapshot, neither taking any lock. The write path (LabelRegistry.Intern of a previously unseen name — a rare event) serialises under a mutex, builds fresh immutable tables extended by one entry, and publishes them — the id→name snapshot before the name→id table — so any reader that observes an id from Lookup can already Resolve it, and any reader that observes an id in a bag observes (by release/acquire ordering through that bag's own publication) tables at least as new as the ones Intern published. Lookup and Resolve therefore never miss a live id.
func NewLabelRegistry ¶
func NewLabelRegistry() *LabelRegistry
NewLabelRegistry returns an empty registry.
func (*LabelRegistry) Intern ¶
func (r *LabelRegistry) Intern(name string) LabelID
Intern returns a stable LabelID for name, allocating one on first encounter. It runs on the write path only (label assignment). A lock-free fast path returns an already-interned id without taking the mutex; only the first interning of a previously unseen name serialises under mu to publish the extended tables. The steady-state label vocabulary is small and stable.
func (*LabelRegistry) Lookup ¶
func (r *LabelRegistry) Lookup(name string) (LabelID, bool)
Lookup returns the LabelID for name and true, or 0 and false when name has not been interned. It is lock-free: it loads the immutable name→id table once and reads it, so concurrent per-row label-predicate lookups never serialise nor bounce a shared reader-count cache line.
type MVCCStats ¶ added in v0.11.0
type MVCCStats struct {
// LabelDeltas, PropDeltas, AdjVersions, EdgeSideVersions and NodeLifeRecords
// are the live record counts per store.
LabelDeltas int64
PropDeltas int64
AdjVersions int64
EdgeSideVersions int64
NodeLifeRecords int64
// IndexRemovalBacklog is the number of label-index removals waiting for the
// watermark. They are memory too, and they are the reason a label bitmap can
// over-report.
IndexRemovalBacklog int64
// AdjConflictStamps is the number of nodes carrying an adjacency write-write
// conflict stamp ([adjVersions]).
//
// It is reported separately from Total rather than added to it, because it is
// not a version: it holds no pre-image, is never read, and takes no part in
// rollback or in a reader's visibility decision. It is write-side bookkeeping
// with the same LIFETIME as a version — bounded by the same watermark in the
// same sweep — so it belongs here to be observable, and folding it into Total
// would misreport the version memory a reader can hold back.
AdjConflictStamps int64
// ConstraintStamps is the number of nodes carrying a per-node CONSTRAINT
// write-write conflict stamp ([constraintVersions], rmp #2353).
//
// Reported separately from Total for the same reason as AdjConflictStamps: it is
// write-side bookkeeping with a version's lifetime, not a version. It stays ZERO
// on any schema declaring no existence constraint, which makes this series the
// direct way for an operator to confirm the constraint stamp is costing an
// unconstrained workload nothing.
ConstraintStamps int64
// Total is the sum of every VERSION count above: the memory the substrate is
// responsible for.
Total int64
// Bound is the number of records that may accumulate from CHURN between
// sweeps in the SETTLED state — that is, the threshold at which the vacuum is
// woken. Total above it means either a reader is holding versions back, which
// is legitimate and is what the two fields below explain, or the vacuum has
// not yet caught up with a burst, which [MVCCStats.Ceiling] bounds.
Bound int64
// Ceiling is the instantaneous bound: the debt at which a committer stops
// merely signalling the vacuum and waits for it.
//
// It exists because the sweep is ASYNCHRONOUS (rmp #2308). Bound alone was a
// true instantaneous bound only while the committer swept before returning; a
// background sweeper can be outrun, so the module states both numbers — the
// one churn settles to, and the one it can never exceed for longer than a
// pass. See [reclaimDebtCeiling].
Ceiling int64
// Watermark is the oldest start timestamp among active readers, or zero when
// reclamation is suspended.
Watermark uint64
// WatermarkRegressions counts times the reclamation watermark moved BACKWARDS,
// and MUST be zero. A non-zero value means a live reader stopped being
// represented in the watermark, so versions it can still reach became
// reclaimable — an Isolation violation rather than a leak. See
// [vacuumState.wmRegress] for why a decrease is impossible while the substrate
// is sound.
WatermarkRegressions int64
// HorizonStaleLeaves counts horizon slots released that nobody held, and MUST be
// zero. It is the RELEASE-side detector of the same family as
// WatermarkRegressions — a slot returned twice, or a slot number released by
// something that never claimed it, whose next release lands on another reader's
// bit and removes that reader from the watermark. See [mvcc.Horizon.StaleLeaves].
//
// Published separately rather than summed with WatermarkRegressions: both must be
// zero, but they are different observations and an operator seeing one number
// cannot tell which of them fired.
HorizonStaleLeaves int64
// Now is the clock's current published instant, so Now-Watermark is how far
// behind the oldest reader is.
Now uint64
// ActiveSnapshots is how many SNAPSHOTS are registered with the horizon.
//
// Readers AND writers, and the name says so (rmp #2312). It was ActiveReaders,
// which was accurate only until rmp #2299 gave a writer a snapshot of its own
// and registered it with the same horizon — after which a graph with one writer
// and no reader reported one "reader". The split an operator actually needs is
// Write.Writers against [MVCCStats.ActiveReaders], both derived from here.
ActiveSnapshots int
// UnregisteredSnapshots is how many active readers or writers could not get a
// horizon slot. While it is non-zero the watermark is zero and NOTHING is
// reclaimed — the one state in which version memory genuinely has no bound,
// and the reason this field exists rather than being inferred from
// unexplained growth.
UnregisteredSnapshots int64
// SnapshotCapacity is how many snapshots may be registered at once. Past it
// reclamation SUSPENDS rather than slowing down, so the utilisation
// ActiveSnapshots/SnapshotCapacity is the number to alert on.
SnapshotCapacity int
// Write is the write side of the substrate: writers in flight, commits,
// aborts and serialization conflicts by store (rmp #2312).
//
// It is the half MVCCStats did not have. Every field above predates
// multi-writer and describes what versioning RETAINS; these describe what
// produces it, and the conflict rate is the signal that tells an operator
// whether their workload is contending at all. See [mvcc.WriteCounters] for
// why the counters are striped and what a striped sum guarantees.
Write mvcc.WriteCounts
// ChainDepth is the distribution of RETAINED version-chain depth: how deep a
// chain a read arriving now may have to walk, per object.
//
// A distribution rather than a mean, because chain depth IS read cost and the
// quantity that matters is the tail — one object with a chain of 200 is a
// latency spike a mean over a million short chains reports as 1.0002. See
// [mvcc.DepthHist] for the bucketing, and for why the reading describes each
// store's most recent complete sweep rather than one instant.
ChainDepth mvcc.Depths
// InFlightCommits is how many commit timestamps have been allocated but
// have not finished: the distance between the instant a reader starts at
// and the newest timestamp handed out.
//
// It is the quantity to look at when readers appear stale, because the
// frontier is CONTIGUOUS — one commit stuck between allocation and
// publication holds it for every reader, however many later commits have
// already published (rmp #2298). It is also what the commit log retains, so
// a value that does not return to zero is both the staleness and the memory
// growth, named once.
InFlightCommits uint64
// SessionsWaiting is how many callers are blocked waiting for the frontier to
// reach their own last commit (rmp #2328).
//
// It is the observable form of the cost the read-side wait moves onto readers. A
// persistently non-zero value says sessions are waiting rather than reading, and
// it is read together with InFlightCommits: the frontier is held back by exactly
// those commits, so the two together say WHO is waiting and WHY.
SessionsWaiting int64
}
MVCCStats is a point-in-time picture of what the versioning substrate is holding and why.
Every field is read with a plain atomic load, so obtaining it costs nothing and disturbs no reader.
func (*MVCCStats) ActiveReaders ¶ added in v0.11.0
ActiveReaders returns how many of the registered snapshots belong to READERS rather than to write transactions.
Derived rather than counted, because the horizon does not distinguish them and giving it a second counter to do so would put a write on the registration path that every read also takes. It can read one low under concurrency — the two quantities are sampled a few nanoseconds apart — and is clamped at zero rather than reported negative.
func (*MVCCStats) OldestSnapshotAge ¶ added in v0.11.0
OldestSnapshotAge returns how far behind the current instant the oldest active snapshot is, in commit timestamps.
It is the quantity to look at when MVCCStats.Total exceeds MVCCStats.Bound: a large value names a long-running transaction as the cause, and MVCCStats.ActiveReaders against MVCCStats.Write says whether it is a reader or a writer.
It is also the watermark age, and there is only one series for both ¶
MVCCStats.Watermark IS the oldest active snapshot's start timestamp, so its age and the oldest snapshot's age are the same number. They are published once (rmp #2312). Publishing them twice under two names would give an operator two names for one quantity, which this module has already had to correct once — see the note in [Graph.publishVacuumMetrics] on the reader count.
func (*MVCCStats) WithinBound ¶ added in v0.11.0
WithinBound reports whether version memory is at or below the SETTLED churn bound — that is, whether nothing is being held back by a reader and the vacuum has caught up.
Because the sweep is asynchronous it is a property of the settled state, not an invariant of every instant: a caller sampling it in the middle of a write burst should expect it to be false and MVCCStats.WithinCeiling to be true.
func (*MVCCStats) WithinCeiling ¶ added in v0.11.0
WithinCeiling reports whether version memory is at or below the instantaneous bound, counting what a reader is legitimately holding back.
Unlike MVCCStats.WithinBound this is meant to hold at every instant of a churn-only workload. A false value with no active reader is the bounded-resources mandate being violated; a false value with an old reader is that reader's cost, which [MVCCStats.OldestReaderAge] attributes.
type NodeValidator ¶ added in v0.2.0
type NodeValidator interface {
ValidateNode(labels []string, props map[string]PropertyValue) error
}
NodeValidator is the optional whole-node enforcement hook. A SchemaValidator installed via Graph.SetValidator that also implements NodeValidator gains required-property/existence enforcement: callers invoke Graph.ValidateNode at the point a node is finalised (after all of its labels and properties are set) to reject a node that violates a whole-node invariant the per-value SchemaValidator.Validate cannot see.
ValidateNode receives the node's complete label set and property bag and returns a non-nil error to reject it. It is satisfied by *schema.Schema, whose github.com/FlavioCFOliveira/GoGraph/graph/lpg/schema.Schema.ValidateNode has the matching signature, so an installed schema enforces required properties through Graph.ValidateNode without any extra wiring.
Implementations must be safe for concurrent use.
type PropertyKeyID ¶
type PropertyKeyID uint32
PropertyKeyID is the compact identifier of an interned property name.
type PropertyKeyRegistry ¶
type PropertyKeyRegistry struct {
// contains filtered or unexported fields
}
PropertyKeyRegistry interns property names and assigns sequential PropertyKeyIDs. It is safe for concurrent use.
Both read paths are fully lock-free: PropertyKeyRegistry.Lookup (name→id) loads the immutable forward table through an atomic.Pointer and PropertyKeyRegistry.Resolve (id→name) loads the immutable id→name snapshot, neither taking any lock. The write path (PropertyKeyRegistry.Intern of a previously unseen name) serialises under a mutex, builds fresh immutable tables extended by one entry, and publishes them — the id→name snapshot first, then the name→id table — so any reader that observes an id from Lookup can already Resolve it, and any reader that observes id in a property bag observes (by release/acquire ordering through that bag's own publication) tables at least as new as the ones Intern published. Lookup/Resolve therefore never miss a live id. Per-row property predicates hit Lookup once per access per reader; making it lock-free removes the RWMutex reader-count atomic that otherwise bounces across cores under concurrent scans. The O(n) copy on intern is a deliberate trade: the property-key vocabulary is append-mostly schema, interned at warm-up and read billions of times.
func NewPropertyKeyRegistry ¶
func NewPropertyKeyRegistry() *PropertyKeyRegistry
NewPropertyKeyRegistry returns an empty registry.
func (*PropertyKeyRegistry) Intern ¶
func (r *PropertyKeyRegistry) Intern(name string) PropertyKeyID
Intern returns a stable PropertyKeyID for name. It runs on the write path only (property assignment). A lock-free fast path returns an already-interned id without taking the mutex; only the first interning of a previously unseen name serialises under mu to publish the extended tables. The steady-state property vocabulary is small and stable.
func (*PropertyKeyRegistry) Lookup ¶
func (r *PropertyKeyRegistry) Lookup(name string) (PropertyKeyID, bool)
Lookup returns the PropertyKeyID for name and true when known. It is lock-free: it loads the immutable name→id table once and reads it, so concurrent per-row property-predicate lookups never serialise nor bounce a shared reader-count cache line.
func (*PropertyKeyRegistry) Resolve ¶
func (r *PropertyKeyRegistry) Resolve(id PropertyKeyID) (string, bool)
Resolve returns the name interned under id. It is lock-free: it loads the immutable id→name snapshot once and indexes into it.
type PropertyKind ¶
type PropertyKind uint8
PropertyKind tags a PropertyValue with its underlying Go type.
const ( PropString PropertyKind = iota + 1 PropInt64 PropFloat64 PropBool PropTime PropBytes PropList // ordered list of PropertyValue elements; v is []PropertyValue )
The supported property kinds. They are stable across releases — new kinds extend this enum; existing values must not be reordered or reused.
type PropertyValue ¶
type PropertyValue struct {
// contains filtered or unexported fields
}
PropertyValue is a tagged union of typed property values. It is laid out as a single (kind, any) pair, totalling 24 bytes on a 64-bit platform regardless of the inhabited variant. The zero value is invalid; values are constructed via the typed constructors (StringValue, Int64Value, etc.).
A PropertyValue is immutable after construction and is copied by value, so it is safe for concurrent reads by multiple goroutines without external locking. The one caveat is the slice-bearing variants: PropertyValue.Bytes and PropertyValue.List return slices that alias the value's backing store, so callers must not mutate the returned slice (doing so would mutate the otherwise-immutable value and break the concurrency guarantee).
func BytesValue ¶
func BytesValue(b []byte) PropertyValue
BytesValue builds a PropBytes wrapping b (no copy).
func DateValue ¶ added in v0.6.0
func DateValue(t time.Time) PropertyValue
DateValue builds a Cypher-visible Date property from t's calendar date — its year, month and day in t's location; any time-of-day and time zone are ignored. The value is the canonical SOH-tagged date string that the columnar storage tier folds into its compact int32 epoch-day column (~4 bytes/value) and that the Cypher read path decodes back to a native Date.
Prefer DateValue over a hand-formatted ISO string (StringValue) for date properties written through the Go API: an untagged string stays in the 16-byte-header string column and reads back as a String, whereas a DateValue costs ~4 bytes/value and round-trips as a Date — the same on-disk and in-memory form a date written through Cypher produces. (Contrast TimeValue/ PropTime, which is not Cypher-visible and reads back as Null.)
func ListValue ¶
func ListValue(elems []PropertyValue) PropertyValue
ListValue builds a PropList from elems. The slice is stored directly (no copy); callers must not modify elems after calling ListValue.
func (PropertyValue) Bool ¶
func (p PropertyValue) Bool() (val, ok bool)
Bool returns the bool value and true when v carries a bool.
func (PropertyValue) Bytes ¶
func (p PropertyValue) Bytes() ([]byte, bool)
Bytes returns the []byte value and true when v carries one. The returned slice aliases the value held by v.
func (PropertyValue) Float64 ¶
func (p PropertyValue) Float64() (float64, bool)
Float64 returns the float64 value and true when v carries a float64.
func (PropertyValue) Int64 ¶
func (p PropertyValue) Int64() (int64, bool)
Int64 returns the int64 value and true when v carries an int64.
func (PropertyValue) Kind ¶
func (p PropertyValue) Kind() PropertyKind
Kind returns the underlying type tag.
func (PropertyValue) List ¶
func (p PropertyValue) List() ([]PropertyValue, bool)
List returns the []PropertyValue elements and true when v carries a PropList. The returned slice aliases the value held by v; callers must not modify it.
func (PropertyValue) String ¶
func (p PropertyValue) String() (string, bool)
String returns the string value and true when v carries a string, the zero value and false otherwise.
type ReadView ¶ added in v0.11.0
type ReadView[N comparable, W any] struct { // contains filtered or unexported fields }
ReadView is a Graph bound to a Snapshot: the same read surface, with every method already resolved as of that instant.
The zero value is not usable; obtain one from Graph.ReadAt.
Safe for concurrent use, on the same terms as the underlying graph: the view is immutable once created and the snapshot it holds is immutable too.
func (*ReadView[N, W]) AdjList ¶ added in v0.11.0
AdjList returns the adjacency backend itself, UNBOUND from this view's instant.
It is the escape hatch for the mapper, the configuration flags, and a bulk scan that needs the whole structure rather than one node — and every caller that reads TOPOLOGY through it owes the snapshot back by hand, using the adjacency's own as-of accessors (adjlist.AdjList.EntryViewAsOf and friends). Reading the current entry instead is not a stale answer, it is an isolation violation, and it is not hypothetical: the CSR pair build did exactly that and let a query observe an edge committed after its snapshot started, while filtering that same pair by liveness resolved AT the snapshot — so the pair belonged to no single instant at all (rmp #2293). It is now built with [csr.BuildFromAdjListAsOf], which resolves every entry at the reader's instant.
Prefer the versioned single-node methods above whenever one node is enough; they cannot be got wrong this way.
func (*ReadView[N, W]) AnyEdgeHandlePropertyEverWritten ¶ added in v0.11.0
AnyEdgeHandlePropertyEverWritten forwards Graph.AnyEdgeHandlePropertyEverWritten. It deliberately does NOT consult this view's snapshot: the latch is a whole-graph, monotonic, never-cleared fact about whether the by-handle property store was ever written, so it is the same answer at every instant and is safe to read through a view. False proves that ReadView.EdgePropertiesByHandle must return empty; true proves nothing beyond "the read is worth making".
func (*ReadView[N, W]) At ¶ added in v0.11.0
At returns a view of the same graph bound to a different snapshot.
func (*ReadView[N, W]) EdgeCreateCount ¶ added in v0.11.0
EdgeCreateCount returns the CURRENT per-pair CREATE multiplicity.
IT IGNORES THIS VIEW'S INSTANT, and that is not an oversight: the counter is unversioned and belongs to no snapshot (rmp #2351). It is exposed here so a caller holding a view does not have to reach past it to the graph, NOT because the value is resolved at the view's instant. See Graph.EdgeCreateCount for what correlating it with anything costs. Not versioned.
func (*ReadView[N, W]) EdgeHasProperty ¶ added in v0.11.0
EdgeHasProperty reports storage presence of a non-null-mapping value at this view's instant.
func (*ReadView[N, W]) EdgeLabels ¶ added in v0.11.0
EdgeLabels returns the pair's derived relationship-type union at this view's instant.
func (*ReadView[N, W]) EdgeLabelsAt ¶ added in v0.11.0
EdgeLabelsAt returns the by-ordinal instance's types at this view's instant.
func (*ReadView[N, W]) EdgeLabelsByHandle ¶ added in v0.11.0
EdgeLabelsByHandle returns the by-handle instance's types at this view's instant.
func (*ReadView[N, W]) EdgeLabelsByHandleID ¶ added in v0.11.0
EdgeLabelsByHandleID is ReadView.EdgeLabelsByHandle keyed by NodeIDs.
func (*ReadView[N, W]) EdgeLabelsByID ¶ added in v0.11.0
EdgeLabelsByID is ReadView.EdgeLabels keyed by NodeIDs.
func (*ReadView[N, W]) EdgeProperties ¶ added in v0.11.0
func (v *ReadView[N, W]) EdgeProperties(src, dst N) map[string]PropertyValue
EdgeProperties returns the pair's coalesced properties at this view's instant.
func (*ReadView[N, W]) EdgePropertiesAt ¶ added in v0.11.0
func (v *ReadView[N, W]) EdgePropertiesAt(src, dst N, idx int64) map[string]PropertyValue
EdgePropertiesAt returns the by-ordinal instance's properties at this view's instant.
func (*ReadView[N, W]) EdgePropertiesByHandle ¶ added in v0.11.0
func (v *ReadView[N, W]) EdgePropertiesByHandle(src, dst N, handle uint64) map[string]PropertyValue
EdgePropertiesByHandle returns the by-handle instance's properties at this view's instant.
func (*ReadView[N, W]) EdgePropertiesByHandleID ¶ added in v0.11.0
func (v *ReadView[N, W]) EdgePropertiesByHandleID(srcID, dstID graph.NodeID, handle uint64) map[string]PropertyValue
EdgePropertiesByHandleID is ReadView.EdgePropertiesByHandle keyed by NodeIDs.
func (*ReadView[N, W]) EdgeWeight ¶ added in v0.11.0
EdgeWeight returns the first matching edge's weight at this view's instant.
func (*ReadView[N, W]) EntryView ¶ added in v0.11.0
EntryView returns every column of id's adjacency entry at this view's instant, resolved from ONE entry so the columns are mutually consistent.
func (*ReadView[N, W]) Exists ¶ added in v0.11.0
Exists is ReadView.IsTombstoned the way round the question is usually asked.
func (*ReadView[N, W]) FirstEdgeHandle ¶ added in v0.11.0
FirstEdgeHandle returns the first matching slot's stable handle at this view's instant.
func (*ReadView[N, W]) ForEachEdgeLabelByID ¶ added in v0.11.0
ForEachEdgeLabelByID streams the pair's derived type union at this view's instant.
func (*ReadView[N, W]) ForEachEdgeProperty ¶ added in v0.11.0
func (v *ReadView[N, W]) ForEachEdgeProperty(src, dst N, visit func(name string, pv PropertyValue))
ForEachEdgeProperty streams the pair's coalesced properties at this view's instant.
func (*ReadView[N, W]) ForEachNodeLabelByID ¶ added in v0.11.0
ForEachNodeLabelByID streams id's label names at this view's instant.
func (*ReadView[N, W]) ForEachSlotRelTypeByID ¶ added in v0.11.0
func (v *ReadView[N, W]) ForEachSlotRelTypeByID(srcID, dstID graph.NodeID, encoded uint32, visit func(name string))
ForEachSlotRelTypeByID streams ONE column-typed slot's relationship types at this view's instant.
func (*ReadView[N, W]) GetEdgeProperty ¶ added in v0.11.0
func (v *ReadView[N, W]) GetEdgeProperty(src, dst N, key string) (PropertyValue, bool)
GetEdgeProperty returns the pair's coalesced value under key at this view's instant.
func (*ReadView[N, W]) GetNodeProperty ¶ added in v0.11.0
func (v *ReadView[N, W]) GetNodeProperty(n N, key string) (PropertyValue, bool)
GetNodeProperty returns the value n carried under key at this view's instant.
func (*ReadView[N, W]) HasConstraints ¶ added in v0.11.0
HasConstraints reports whether any schema constraint is registered.
func (*ReadView[N, W]) HasEdge ¶ added in v0.11.0
HasEdge reports whether a directed edge existed at this view's instant.
func (*ReadView[N, W]) HasEdgeByID ¶ added in v0.11.0
HasEdgeByID is ReadView.HasEdge keyed by NodeIDs.
func (*ReadView[N, W]) HasEdgeHandleLabelRecordByID ¶ added in v0.11.0
func (v *ReadView[N, W]) HasEdgeHandleLabelRecordByID(srcID, dstID graph.NodeID, handle uint64) bool
HasEdgeHandleLabelRecordByID reports whether a handle-keyed type record existed for the slot at this view's instant, which is the precedence question that decides whether a slot is handle-typed or column-typed.
func (*ReadView[N, W]) HasEdgeLabel ¶ added in v0.11.0
HasEdgeLabel reports whether the pair carried the named type at this view's instant.
func (*ReadView[N, W]) HasNodeLabel ¶ added in v0.11.0
HasNodeLabel reports whether n carried the named label at this view's instant.
func (*ReadView[N, W]) HasNodeLabelByID ¶ added in v0.11.0
HasNodeLabelByID is ReadView.HasNodeLabel keyed by NodeID.
func (*ReadView[N, W]) IndexManager ¶ added in v0.11.0
IndexManager returns the secondary-index manager, a candidate source read at the PRESENT.
Its candidates ARE re-checked against the versioned property store, so a seek cannot return a node that did not match at the reader's instant. That was asserted here without evidence until it was measured: the check is cypher.TestIndexSeek_SelfContradictionUnderConcurrentWrites, which seeks by an indexed property and then asserts the SAME property in a WHERE clause — a contradiction that can only survive if the two answered at different instants. Roughly two thousand observations per run against a writer churning the indexed nodes produce zero, and the test carries sensitivity controls so it cannot pass vacuously.
func (*ReadView[N, W]) IsTombstoned ¶ added in v0.11.0
IsTombstoned reports whether id was ABSENT at this view's instant.
It is the negation of Graph.NodeExistsAsOf, kept under the old name because every call site asks it that way round. It is versioned as of P4c (rmp #2290): a node created after this view started reads as absent, and one deleted after it reads as present — the tombstone bitmap alone can express neither.
func (*ReadView[N, W]) LiveNodeCountExact ¶ added in v0.11.0
LiveNodeCountExact returns the live node count and whether it is EXACT for this view's instant.
It is exact when no node has been born or has died recently enough for this reader to disagree with the present, which is every read-only workload and every workload whose churn the reclaimer has caught up with. When it is not exact the caller must count by scanning, which is snapshot-correct because the scan itself is.
Without this the O(1) count pushdown answers `MATCH (n) RETURN count(*)` from the present while every other clause of the same query answers from the snapshot.
func (*ReadView[N, W]) LiveNodeFilter ¶ added in v0.11.0
LiveNodeFilter returns the liveness predicate at this view's instant.
func (*ReadView[N, W]) LiveOrder ¶ added in v0.11.0
LiveOrder returns the CURRENT live node count.
NOT versioned, and it is the one candidate structure that cannot simply be re-checked: it is a COUNT, so there is no object to verify it against. Its only uses are cardinality estimation, where an estimate is what is wanted, and the O(1) count pushdown — which ReadView.LiveNodeCountExact gates.
func (*ReadView[N, W]) NodeIndex ¶ added in v0.11.0
NodeIndex returns the label bitmap index, which is a candidate source read at the PRESENT and re-checked against the versioned label bags above.
func (*ReadView[N, W]) NodeLabels ¶ added in v0.11.0
NodeLabels returns n's label names at this view's instant, in unspecified order.
func (*ReadView[N, W]) NodeLabelsByID ¶ added in v0.11.0
NodeLabelsByID is ReadView.NodeLabels keyed by NodeID.
func (*ReadView[N, W]) NodeProperties ¶ added in v0.11.0
func (v *ReadView[N, W]) NodeProperties(n N) map[string]PropertyValue
NodeProperties returns n's properties at this view's instant.
func (*ReadView[N, W]) NodePropertiesByIDFunc ¶ added in v0.11.0
func (v *ReadView[N, W]) NodePropertiesByIDFunc(id graph.NodeID, visit func(name string, pv PropertyValue))
NodePropertiesByIDFunc streams id's properties at this view's instant.
func (*ReadView[N, W]) NodePropertyByID ¶ added in v0.11.0
NodePropertyByID is ReadView.GetNodeProperty keyed by NodeID.
func (*ReadView[N, W]) OutDegreeBoundedByID ¶ added in v0.11.0
OutDegreeBoundedByID counts id's live out-edges at this view's instant, capped at limit.
func (*ReadView[N, W]) OutDegreeByTypeBoundedByID ¶ added in v0.11.0
func (v *ReadView[N, W]) OutDegreeByTypeBoundedByID(id graph.NodeID, relType LabelID, limit int) (int, bool)
OutDegreeByTypeBoundedByID counts id's live typed out-edges at this view's instant, capped at limit.
func (*ReadView[N, W]) OutDegreeMatchingBoundedByID ¶ added in v0.11.0
func (v *ReadView[N, W]) OutDegreeMatchingBoundedByID( id graph.NodeID, relType LabelID, typed bool, limit int, farOK func(graph.NodeID) bool, ) (int, bool)
OutDegreeMatchingBoundedByID counts id's live out-edges whose far endpoint satisfies farOK at this view's instant, capped at limit.
func (*ReadView[N, W]) PropertyKeys ¶ added in v0.11.0
func (v *ReadView[N, W]) PropertyKeys() *PropertyKeyRegistry
PropertyKeys returns the property-key registry.
func (*ReadView[N, W]) Raw ¶ added in v0.11.0
Raw returns the underlying graph, unbound.
It is the escape hatch for a caller that needs the mapper, a registry, a configuration flag, or the write surface. It is named to be greppable: a review looking for reads that escaped the snapshot looks for this.
func (*ReadView[N, W]) Registry ¶ added in v0.11.0
func (v *ReadView[N, W]) Registry() *LabelRegistry
Registry returns the label registry.
func (*ReadView[N, W]) Snapshot ¶ added in v0.11.0
Snapshot returns the instant this view reads at, or nil for a view that reads the current value.
func (*ReadView[N, W]) StoreConstraints ¶ added in v0.11.0
func (v *ReadView[N, W]) StoreConstraints() []StoreConstraint
StoreConstraints returns the store-direct constraint declarations.
func (*ReadView[N, W]) TopoGeneration ¶ added in v0.11.0
TopoGeneration returns the topology epoch, which keys the derived caches.
type SchemaValidator ¶
type SchemaValidator interface {
Validate(propertyName string, value PropertyValue) error
}
SchemaValidator is the interface that schema enforcement hooks implement. It is satisfied by *schema.Schema after properties have been registered.
Validate receives the property name and the value about to be written. A nil return allows the write; a non-nil return rejects it with the returned error, leaving the graph state unchanged.
Validate enforces only per-property typing — a single value examined in isolation — because it runs at the mutation point, where the node is not yet complete (a node acquires its labels and properties one mutation at a time; see Graph.SetNodeProperty). Whole-node invariants such as required-property existence cannot be decided from one value and are enforced separately by NodeValidator/Graph.ValidateNode at the node-finalisation boundary.
Implementations must be safe for concurrent use.
type Session ¶ added in v0.11.0
type Session[N comparable, W any] struct { // contains filtered or unexported fields }
Session is one caller's sequence of operations against a graph, and the unit read-your-own-writes is guaranteed within.
Obtain one from Graph.NewSession. A Session is cheap — one word of state — so a server may hold one per connection, and an embedded caller one per goroutine that writes and then reads.
The zero value is not usable. Safe for concurrent use, though a Session shared between goroutines only promises that each of them observes every commit ANY of them has made through it, which is usually more coupling than a caller wants.
func (*Session[N, W]) ApplyVersioned ¶ added in v0.11.0
ApplyVersioned runs fn as one write transaction that observes every commit this session has made, and records its own commit instant on the session.
It is Graph.ApplyVersioned plus the session's guarantee at both ends: the transaction waits for this session's previous commits to become visible before it takes its start timestamp, and publishes its own instant into the session on the way out.
The wait is what closes the SPURIOUS SELF-CONFLICT: without it, a session's next transaction can start below its own previous commit, find its own version at the chain head and be refused with a serialization error on a key no other transaction ever touched.
Safe for concurrent use; each goroutine should use its own session.
func (*Session[N, W]) ApplyVersionedCtx ¶ added in v0.11.0
ApplyVersionedCtx is Session.ApplyVersioned with both the frontier wait and the barrier acquisition bounded by ctx.
Safe for concurrent use; each goroutine should use its own session.
func (*Session[N, W]) Await ¶ added in v0.11.0
Await blocks until the visible frontier has reached this session's floor, so a snapshot taken next observes every commit the session has made.
It is the WAIT alone, without the snapshot Session.BeginReadCtx returns. A caller that wants the guarantee for an operation that takes its OWN snapshot — a query engine running a statement, say — must not hold a second one meanwhile: an unused snapshot still occupies a horizon slot and pins reclamation for as long as it is open.
A session that has committed nothing returns immediately after one atomic load.
Safe for concurrent use.
func (*Session[N, W]) BeginRead ¶ added in v0.11.0
BeginRead opens a read snapshot that observes every commit this session has made.
It is Graph.BeginRead plus the session's guarantee: if this session has committed at an instant the frontier has not yet reached, it waits for it. The caller MUST pass the result to Graph.EndRead exactly once, exactly as with the graph's own form.
It cannot be cancelled; use Session.BeginReadCtx for a caller with a deadline.
Safe for concurrent use.
func (*Session[N, W]) BeginReadCtx ¶ added in v0.11.0
BeginReadCtx is Session.BeginRead with the wait bounded by ctx.
When ctx finishes first it returns a nil snapshot and ctx's error, and NOTHING is registered — a nil snapshot is safe to pass to Graph.EndRead, so a caller may still defer it unconditionally.
Safe for concurrent use.
func (*Session[N, W]) BeginVersionedTx ¶ added in v0.11.0
BeginVersionedTx opens a multi-statement write transaction that observes every commit this session has made.
The caller MUST close it with exactly one Session.EndVersionedTx, which is what records the transaction's instant on the session. Closing it with Graph.EndVersionedTx instead publishes correctly but does NOT advance the session's floor, so the session loses its guarantee from that point on.
Safe for concurrent use; each goroutine should use its own session.
func (*Session[N, W]) BeginVersionedTxCtx ¶ added in v0.11.0
BeginVersionedTxCtx is Session.BeginVersionedTx with the frontier wait bounded by ctx. When ctx finishes first it returns the zero transaction and ctx's error, and no transaction is opened.
Safe for concurrent use; each goroutine should use its own session.
func (*Session[N, W]) EndVersionedTx ¶ added in v0.11.0
EndVersionedTx closes a transaction opened with Session.BeginVersionedTx and records its commit instant on the session.
Idempotent for the zero transaction, exactly as Graph.EndVersionedTx is.
func (*Session[N, W]) Floor ¶ added in v0.11.0
Floor returns the instant this session must observe: the newest commit it has made, or zero if it has made none.
Exported so a caller that opens a transaction through some other path can carry the guarantee forward, and so a test can assert what a session pinned.
type Snapshot ¶ added in v0.11.0
type Snapshot struct {
// contains filtered or unexported fields
}
Snapshot is a consistent read view of the graph at one instant.
Obtain one with Graph.BeginRead and release it with Graph.EndRead, exactly once, on every path including error and panic ones — a snapshot that is never released holds the reclamation watermark and versions accumulate behind it.
A nil *Snapshot passed to a versioned accessor means "the current stored value", which is what a writer inside the visibility barrier needs.
Safe for concurrent use by readers: it is immutable once returned.
type StoreConstraint ¶ added in v0.8.0
type StoreConstraint struct {
// Label is the constrained node label.
Label string
// Property is the constrained property key.
Property string
// Kind is the constraint kind (0 = UNIQUE, 1 = NOT NULL), matching the
// txn package's ConstraintKind ordinals.
Kind uint8
}
StoreConstraint is a durable schema-constraint slot recorded on the graph by the txn.Store apply path or by recovery (see Graph.AddStoreConstraint). It carries the constraint's enforcement identity — kind, label, property — but not its user-defined name, which the store-direct path does not retain.
type VacuumStats ¶ added in v0.11.0
type VacuumStats struct {
// Running says a sweeper goroutine is alive right now.
Running bool
// Starts and Exits are how many times a sweeper has been spawned and has
// terminated. They differ by at most one, and by exactly one while Running.
Starts uint64
Exits uint64
// Passes is how many sweeps have run, and Reclaimed how many records they
// released in total.
Passes uint64
Reclaimed int64
// CappedPasses is how many passes stopped at the per-pass record bound.
CappedPasses uint64
// Backlog is the reclamation debt not yet swept — versions created since the
// last pass began.
Backlog int64
// RecordsPerPass is the explicit per-pass upper bound on work.
RecordsPerPass int
// PassTotal is the time spent inside passes, so PassTotal/Passes is the mean
// pass duration. The distribution is published as the latency series
// "lpg.mvcc.vacuum.pass"; this field is what a caller with no histogram
// backend can still read (rmp #2312).
PassTotal time.Duration
}
VacuumStats is a point-in-time picture of the background vacuum.
It is the vacuum's half of what Graph.MVCCStats reports about the substrate: that says how much is retained and why, this says what the sweep has been doing about it.
func (*VacuumStats) MeanPass ¶ added in v0.11.0
func (s *VacuumStats) MeanPass() time.Duration
MeanPass returns the average duration of a completed vacuum pass, or zero when none has completed.
type WriteTx ¶ added in v0.11.0
type WriteTx struct {
// contains filtered or unexported fields
}
WriteTx names one open write transaction to a caller in another package.
It is what Graph.ApplyVersioned hands its closure, and what the Cypher engine's write path carries so that its reads resolve through its OWN transaction rather than through whichever transaction the graph's slot happens to name (rmp #2304). Memgraph threads `Transaction *transaction` into every accessor for the same reason (memgraph/memgraph, branch master, read 2026-08-02; src/storage/v2/).
The zero value names no transaction, which reads as "the present" — correct for a direct mutation outside any transaction, and wrong inside one, so a caller inside a bracket must pass the value it was given rather than the zero.
It is valid only while its bracket is open, and it must not be retained past it: the state it names is recycled on the unwind.
func (WriteTx) EnterUndo ¶ added in v0.11.0
func (tx WriteTx) EnterUndo()
EnterUndo marks the start of this transaction's PHYSICAL undo replay, during which its writes are withdrawals of work it already applied rather than new updates.
It must be paired with exactly one WriteTx.ExitUndo, and the region must cover the whole replay. Inside it, a write is no longer refused merely because the transaction is doomed — which it always is when an undo has to run — while the per-object head test still applies, so an inverse can withdraw this transaction's own versions and nothing else. [writeCtx.undoing] carries the full reasoning, the prior art and the lost update this closes.
Both are no-ops on the zero value, so a caller can bracket unconditionally.
The region must not be entered concurrently from two goroutines, which is already the contract for driving one write transaction.
func (WriteTx) Err ¶ added in v0.11.0
Err returns the serialization conflict this transaction has been doomed by, or nil when it is still viable. The error wraps mvcc.ErrSerializationConflict and carries the store attribution through mvcc.Conflict.
Why an embedder needs this and cannot do without it ¶
Most primitives report a conflict by returning it, so a caller learns at the statement. But a conflict hit by a primitive that CANNOT return an error — a label removal, a property delete, any of the five per-edge side stores — is recorded on the transaction instead, and the only way to observe it is to ask (rmp #2300).
[labelTx.commit] asks, which is what makes the substrate's own transactions safe. An embedder that drives the write bracket itself — cypher's ExplicitTx and its autocommit path both do — must ask too, and BEFORE it makes anything durable, or a transaction whose only conflicting write went through such a primitive commits successfully having dropped it. That is a lost update with nothing anywhere reporting it, and it is what rmp #2354 measured: a `REMOVE n:Label` that collided with a peer's uncommitted removal returned nil from the statement AND nil from Commit, and the label was still there afterwards.
Memgraph reads the same record in the same place — Storage::Commit tests `transaction_.must_abort` and returns SerializationError (src/storage/v2/storage.cpp).
Nil on the zero value, so a caller can ask unconditionally.
func (WriteTx) ExitUndo ¶ added in v0.11.0
func (tx WriteTx) ExitUndo()
ExitUndo ends the region WriteTx.EnterUndo opened, restoring the ordinary rule that a doomed transaction refuses further writes.
func (WriteTx) Valid ¶ added in v0.11.0
Valid reports whether tx names an open write transaction.
It is false for the zero value and for a bracket opened on a graph whose versioning substrate is disarmed (see [Graph.disarmMVCCForTest]), where there is no transaction to name and every write is committed as it is made.
type WriteView ¶ added in v0.11.0
type WriteView[N comparable, W any] struct { // contains filtered or unexported fields }
WriteView is a Graph bound to ONE write transaction: every mutation made through it stamps its versions with that transaction's shared commit record, tests write-write conflicts against that transaction's snapshot, and claims an adjacency shard's copy-on-write builder under that transaction's identity.
Obtain one with Graph.Writer. A view built from the zero WriteTx carries no transaction and behaves exactly as the graph's own mutators do — each write is its own transaction, committed the instant it is made — which is the right answer for a caller outside any bracket and the wrong one inside one.
It is valid only while its transaction's bracket is open and must NOT be retained past it: the state it names is recycled on the unwind. A retained view does not corrupt anything — a retracted transaction's writes fall back to a fresh untransacted timestamp — but it silently stops being transactional.
It carries no mutable state of its own, so it is safe for concurrent use exactly as far as the graph and the transaction it names are: two goroutines must not drive ONE write transaction concurrently, and two goroutines holding their own transactions may write concurrently.
func (WriteView[N, W]) AddEdge ¶ added in v0.11.0
AddEdge is Graph.AddEdge inside this view's transaction.
func (WriteView[N, W]) AddEdgeH ¶ added in v0.11.0
AddEdgeH is Graph.AddEdgeH inside this view's transaction.
func (WriteView[N, W]) AddEdgeHIfAbsent ¶ added in v0.11.0
AddEdgeHIfAbsent is Graph.AddEdgeHIfAbsent inside this view's transaction.
func (WriteView[N, W]) AddNode ¶ added in v0.11.0
AddNode is Graph.AddNode inside this view's transaction.
func (WriteView[N, W]) DelEdgeProperty ¶ added in v0.11.0
DelEdgeProperty is Graph.DelEdgeProperty inside this view's transaction.
func (WriteView[N, W]) DelEdgePropertyByHandle ¶ added in v0.11.0
DelEdgePropertyByHandle is Graph.DelEdgePropertyByHandle inside this view's transaction.
func (WriteView[N, W]) DelNodeProperty ¶ added in v0.11.0
DelNodeProperty is Graph.DelNodeProperty inside this view's transaction.
func (WriteView[N, W]) Graph ¶ added in v0.11.0
Graph returns the graph this view writes to, for the read-side and bookkeeping methods that need no transaction — side-effect counters, the label and property-key registries, the secondary indexes.
A read that must observe the transaction's own uncommitted work goes through Graph.WriterViewOf instead, which is what the snapshot on the carried transaction is for.
func (WriteView[N, W]) NoteConstraintTouch ¶ added in v0.11.0
NoteConstraintTouch records that this transaction made a write to n that could INTRODUCE a property-existence violation — a property removal, a label gain, a node creation — and reports the write-write conflict it hit, or nil.
Why an embedder must call this, and only sometimes ¶
Conflict detection here is per SUBSTORE, so two transactions writing DIFFERENT substores of one node never meet. A NOT NULL constraint binds a label to a property — two substores — so write skew across them committed a state violating the declared invariant while neither transaction violated it on its own snapshot (rmp #2353). This is the seam that makes such a pair collide: both halves stamp the same per-node slot, so the second one to arrive is refused.
CALL IT ONLY FOR NODES AN EXISTENCE CONSTRAINT ACTUALLY COVERS. The stamp is node-granular — every reference engine's granularity for this, because PostgreSQL and InnoDB version the whole row and Memgraph the whole vertex — and node granularity conflicts more than substore granularity does. Applying it to every write would raise the conflict rate for the majority of workloads, which declare no existence constraint and cannot suffer the anomaly at all. cypher gates it on the same [exec.ConstraintRegistry.HasAnyNotNull] test that decides whether to record touched nodes, so an unconstrained schema never calls in.
The conflict is RECORDED on the transaction as well as returned, so a caller that cannot report one still dooms the transaction and commit refuses to publish it; see WriteTx.Err. A view carrying no transaction, or a key that was never interned, is a no-op returning nil.
func (WriteView[N, W]) Read ¶ added in v0.11.0
Read returns the graph as this view's transaction READS it: as of the instant the transaction began, plus the versions the transaction has written itself.
A write path that reads — a DELETE enumerating the labels it must strip, a MERGE testing what already exists — must read through this and not through WriteView.Graph, which answers with the current stored value and therefore with other in-flight writers' uncommitted work. It is Graph.WriterViewOf for a caller that already holds the view, so the snapshot comes from the carried transaction rather than from the graph's slot.
A view carrying no transaction reads the present, which is the correct answer outside a bracket.
func (WriteView[N, W]) RemoveAllEdgesFrom ¶ added in v0.11.0
func (wv WriteView[N, W]) RemoveAllEdgesFrom(src N)
RemoveAllEdgesFrom is Graph.RemoveAllEdgesFrom inside this view's transaction.
func (WriteView[N, W]) RemoveEdge ¶ added in v0.11.0
func (wv WriteView[N, W]) RemoveEdge(src, dst N)
RemoveEdge is Graph.RemoveEdge inside this view's transaction.
func (WriteView[N, W]) RemoveEdgeByHandle ¶ added in v0.11.0
RemoveEdgeByHandle is Graph.RemoveEdgeByHandle inside this view's transaction.
func (WriteView[N, W]) RemoveEdgeInstance ¶ added in v0.11.0
RemoveEdgeInstance is Graph.RemoveEdgeInstance inside this view's transaction.
func (WriteView[N, W]) RemoveEdgeInstanceByHandle ¶ added in v0.11.0
RemoveEdgeInstanceByHandle is Graph.RemoveEdgeInstanceByHandle inside this view's transaction.
func (WriteView[N, W]) RemoveEdgeLabel ¶ added in v0.11.0
RemoveEdgeLabel is Graph.RemoveEdgeLabel inside this view's transaction.
func (WriteView[N, W]) RemoveNode ¶ added in v0.11.0
func (wv WriteView[N, W]) RemoveNode(n N)
RemoveNode is Graph.RemoveNode inside this view's transaction.
func (WriteView[N, W]) RemoveNodeLabel ¶ added in v0.11.0
RemoveNodeLabel is Graph.RemoveNodeLabel inside this view's transaction.
func (WriteView[N, W]) Revive ¶ added in v0.11.0
func (wv WriteView[N, W]) Revive(n N)
Revive is Graph.Revive inside this view's transaction.
func (WriteView[N, W]) SetEdgeLabel ¶ added in v0.11.0
SetEdgeLabel is Graph.SetEdgeLabel inside this view's transaction.
func (WriteView[N, W]) SetEdgeLabelAt ¶ added in v0.11.0
SetEdgeLabelAt is Graph.SetEdgeLabelAt inside this view's transaction.
func (WriteView[N, W]) SetEdgeLabelByHandle ¶ added in v0.11.0
SetEdgeLabelByHandle is Graph.SetEdgeLabelByHandle inside this view's transaction.
func (WriteView[N, W]) SetEdgeProperty ¶ added in v0.11.0
func (wv WriteView[N, W]) SetEdgeProperty(src, dst N, key string, value PropertyValue) error
SetEdgeProperty is Graph.SetEdgeProperty inside this view's transaction.
func (WriteView[N, W]) SetEdgePropertyAt ¶ added in v0.11.0
func (wv WriteView[N, W]) SetEdgePropertyAt(src, dst N, idx int64, key string, value PropertyValue) error
SetEdgePropertyAt is Graph.SetEdgePropertyAt inside this view's transaction.
func (WriteView[N, W]) SetEdgePropertyByHandle ¶ added in v0.11.0
func (wv WriteView[N, W]) SetEdgePropertyByHandle(src, dst N, handle uint64, key string, value PropertyValue) error
SetEdgePropertyByHandle is Graph.SetEdgePropertyByHandle inside this view's transaction.
func (WriteView[N, W]) SetNodeLabel ¶ added in v0.11.0
SetNodeLabel is Graph.SetNodeLabel inside this view's transaction.
func (WriteView[N, W]) SetNodeProperty ¶ added in v0.11.0
func (wv WriteView[N, W]) SetNodeProperty(n N, key string, value PropertyValue) error
SetNodeProperty is Graph.SetNodeProperty inside this view's transaction.
Source Files
¶
- edge_create_count.go
- edge_handle.go
- edge_handle_durable.go
- edge_instance_labels.go
- edge_instance_props.go
- edge_labels.go
- edge_property.go
- edge_property_column.go
- edge_slot_reltype.go
- index_manager_atomic.go
- instmap.go
- introspect.go
- labelbag.go
- lpg.go
- mvcc_abort_reclaim.go
- mvcc_abort_sides.go
- mvcc_adjversion.go
- mvcc_constraintversion.go
- mvcc_depth.go
- mvcc_edge_side.go
- mvcc_gc.go
- mvcc_index.go
- mvcc_labels.go
- mvcc_life.go
- mvcc_metricnames.go
- mvcc_props.go
- mvcc_reclaim.go
- mvcc_sidemap.go
- mvcc_stats.go
- mvcc_txn.go
- mvcc_vacuum.go
- mvcc_write.go
- mvcc_writectx.go
- propbag.go
- property.go
- readview.go
- reentrancy_disabled.go
- session.go
- snapshot.go
- snapshot_read.go
- validator.go
- writeview.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package schema declares the optional type schema for a labelled property graph: which labels exist, which property keys exist, which lpg.PropertyKind each property carries, and which properties each label requires.
|
Package schema declares the optional type schema for a labelled property graph: which labels exist, which property keys exist, which lpg.PropertyKind each property carries, and which properties each label requires. |