index

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 28, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package index coordinates the secondary indexes attached to a labelled property graph.

A Manager owns a set of named indexes (label bitmap, hash exact-match, B+ tree range) and fans out mutations to every index that subscribes to the affected property or label. The fan-out is best-effort sequential: failures in one subscriber do not abort the others (subscribers are independent and idempotent).

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrIndexCorrupted = errors.New("index: serialized form corrupted")

ErrIndexCorrupted is returned by Serializer.Deserialize when the serialised form is structurally malformed or its CRC32C trailer does not match the payload. Callers (snapshot recovery in particular) treat this as "rebuild from the LPG" rather than as a fatal error.

View Source
var ErrIndexExists = errors.New("index: an index by that name already exists")

ErrIndexExists is returned by Manager.CreateIndex when the name is already in use.

View Source
var ErrIndexNotFound = errors.New("index: no index by that name")

ErrIndexNotFound is returned by Manager.DropIndex or Manager.GetIndex when the named index does not exist.

View Source
var ErrIndexValueTypeUnsupported = errors.New("index: value type not supported for serialization")

ErrIndexValueTypeUnsupported is returned by a generic index's Serialize / Deserialize methods when the value-type parameter is not in the supported on-disk encoding set (currently: string). Callers can convert their value type to string before registering the index for snapshot durability.

Functions

This section is empty.

Types

type Change

type Change struct {
	Op       ChangeOp
	Node     graph.NodeID
	Dst      graph.NodeID // edge changes only
	Property uint32       // 0 when not a property change
	Label    uint32       // 0 when not a label change

	// OldValue and NewValue are present only for property changes.
	// They are typed as any so this package stays generic across
	// every PropertyValue kind without importing the lpg package.
	OldValue any
	NewValue any
}

Change describes a single mutation observed by the Manager. Each subscriber inspects the relevant fields and decides whether to update its own state.

Property and Label fields are interned identifiers from the owning graph's registries (lpg.PropertyKeyID / lpg.LabelID), surfaced as uint32 so this package does not import the lpg package and create a cycle.

func (Change) IsEdgeChange

func (c Change) IsEdgeChange() bool

IsEdgeChange reports whether the change concerns an edge.

type ChangeOp

type ChangeOp uint8

ChangeOp tags the shape of a Change.

const (
	OpAddNodeLabel ChangeOp = iota + 1
	OpRemoveNodeLabel
	OpSetNodeProperty
	OpDelNodeProperty
	OpAddEdgeLabel
	OpRemoveEdgeLabel
	OpSetEdgeProperty
	OpDelEdgeProperty
)

Mutation kinds the Manager can fan out.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager owns the set of named indexes attached to a graph and fans out mutations to every subscriber.

Manager is safe for concurrent use.

Example

ExampleManager shows the Manager lifecycle: register a concrete index under a name, list and count the registered indexes, and reject a duplicate registration with ErrIndexExists.

package main

import (
	"errors"
	"fmt"

	"github.com/FlavioCFOliveira/GoGraph/graph/index"
	"github.com/FlavioCFOliveira/GoGraph/graph/index/label"
)

func main() {
	m := index.NewManager()

	if err := m.CreateIndex("by_label", label.NewNodeIndex()); err != nil {
		fmt.Println("unexpected:", err)
	}

	// Re-registering the same name is rejected.
	err := m.CreateIndex("by_label", label.NewNodeIndex())
	fmt.Println("duplicate is ErrIndexExists:", errors.Is(err, index.ErrIndexExists))

	fmt.Println("count:", m.Count())
	fmt.Println("names:", m.ListIndexes())
}
Output:
duplicate is ErrIndexExists: true
count: 1
names: [by_label]

func NewManager

func NewManager() *Manager

NewManager returns an empty Manager.

func (*Manager) Apply

func (m *Manager) Apply(c Change)

Apply fans c out to every registered subscriber under a read lock so subscribers cannot be unregistered mid-update. The Manager itself does not enforce ordering across subscribers — each subscriber is expected to be order-independent on the change stream it observes.

Example

ExampleManager_Apply shows the Manager fanning a change out to every registered subscriber. The label index observes OpAddNodeLabel events and can then be queried back through GetIndex for the NodeIDs that carry a given label.

package main

import (
	"fmt"

	"github.com/FlavioCFOliveira/GoGraph/graph"
	"github.com/FlavioCFOliveira/GoGraph/graph/index"
	"github.com/FlavioCFOliveira/GoGraph/graph/index/label"
)

func main() {
	const labelPerson = uint32(7)

	m := index.NewManager()
	_ = m.CreateIndex("node_labels", label.NewNodeIndex())

	// A mutation observed by the owning graph is fanned out to every
	// subscriber. Here two nodes acquire the Person label.
	m.Apply(index.Change{Op: index.OpAddNodeLabel, Node: graph.NodeID(1), Label: labelPerson})
	m.Apply(index.Change{Op: index.OpAddNodeLabel, Node: graph.NodeID(4), Label: labelPerson})

	// Recover the concrete index to run a query.
	sub, _ := m.GetIndex("node_labels")
	idx := sub.(*label.Index)

	fmt.Println("kind:", idx.Kind())
	fmt.Println("Person count:", idx.Count(labelPerson))
	fmt.Println("Person members:", idx.Scan(labelPerson))
}
Output:
kind: label
Person count: 2
Person members: [1 4]

func (*Manager) ApplyBatch

func (m *Manager) ApplyBatch(changes []Change)

ApplyBatch fans an ordered slice of changes out to every subscriber in order. The whole batch is applied under one read lock; this is the substrate consumed by future transaction integration (Sprint 3).

func (*Manager) Count

func (m *Manager) Count() int

Count returns the number of currently registered indexes. It is safe to call on a nil Manager and returns 0 in that case.

func (*Manager) CreateIndex

func (m *Manager) CreateIndex(name string, sub Subscriber) error

CreateIndex registers sub under name. Returns ErrIndexExists when the name is already taken.

func (*Manager) DropIndex

func (m *Manager) DropIndex(name string) error

DropIndex removes the named index.

func (*Manager) GetIndex

func (m *Manager) GetIndex(name string) (Subscriber, error)

GetIndex returns the subscriber registered under name. It is safe to call on a nil Manager and returns ErrIndexNotFound in that case.

func (*Manager) ListIndexes

func (m *Manager) ListIndexes() []string

ListIndexes returns the names of every currently registered index in unspecified order. It is safe to call on a nil Manager and returns nil in that case.

type NodeSet added in v0.6.0

type NodeSet struct {
	// contains filtered or unexported fields
}

NodeSet is the per-key node-set representation shared by the btree, hash, and label indexes. The zero value is a valid empty set. See the package-level nodeset.go documentation for the state machine and the query/serialization/GC-safety invariants.

Concurrency: a NodeSet is not safe for concurrent use on its own. It is embedded by value in an index (a btree leaf slot, a hash shard map, or the label-index map), and every read and mutation is serialised by that owning index's lock; a NodeSet is never shared across goroutines outside that discipline. Lookup paths that hand a set's contents to a caller copy out under the read lock, so the returned data is safe for concurrent use.

The two fields form a tagged union resolved solely by meta's low two bits (see the state* constants). ptr is GC-scanned and is always nil or a real Go pointer; it never carries tag bits.

func NodeSetFromBitmap added in v0.6.0

func NodeSetFromBitmap(bm *roaring64.Bitmap) NodeSet

NodeSetFromBitmap returns the cheapest NodeSet representation of bm. A bitmap whose cardinality fits the inline small-set tier is down-converted (its few sorted ids extracted) so a sparse entry reloaded from a roaring image regains the memory win; a denser bitmap is kept on the bitmap tier WITHOUT extracting its (potentially huge) id array, so a dense label costs no transient O(cardinality) slice. Ownership of bm transfers to the set when it is kept; when down-converted, bm is no longer referenced.

It is the label index's deserialization adaptor: that index persists the roaring native image, so it reads back a bitmap and calls this to recover the tiered in-memory shape (sprint 206, #1585).

func NodeSetFromSorted added in v0.6.0

func NodeSetFromSorted(ids []uint64) NodeSet

NodeSetFromSorted builds a NodeSet from an already strictly-ascending id slice. It is the deserialization constructor: the btree and hash readers parse the logical sorted NodeID list and hand it here, getting the cheapest representation for that cardinality (singleton/small/ bitmap) without re-sorting. The caller guarantees ids is sorted ascending with no duplicates.

func (*NodeSet) Add added in v0.6.0

func (s *NodeSet) Add(node uint64) (wasEmpty bool)

Add inserts node into the set, preserving ascending order and promoting to a bitmap when the small tier would overflow. Adding a node already present is a no-op (set semantics). Returns true when the set was previously empty (so a caller maintaining a distinct-key count can detect a brand-new entry).

func (*NodeSet) AddRange added in v0.6.0

func (s *NodeSet) AddRange(from, to uint64)

AddRange adds every id in [from, to] (inclusive) to the set. It always promotes to (or stays) a bitmap and uses roaring's run-container AddRange, so a contiguous band of NodeIDs is stored in O(1) space. This is the bulk-ingest fast path the label index relies on for dense labels; it is intentionally the ONLY entry point that can create a bitmap without first crossing smallSetMax, and a set that takes an AddRange is permanently a bitmap (#1585).

func (*NodeSet) AppendTo added in v0.6.0

func (s *NodeSet) AppendTo(dst []uint64) []uint64

AppendTo appends every NodeID in strictly ascending order — the same order as ToArray — to dst and returns the extended slice, WITHOUT materialising a throwaway bitmap for the inline (singleton/small) states. It is the allocation-light way to drain a set into a caller-owned buffer under the index read lock: a singleton or small set appends straight from the inline fields, so a caller whose dst has spare capacity (e.g. a reused seek buffer) pays no heap allocation at all. Only the promoted bitmap state allocates a single iterator. The appended ids are an independent snapshot the caller may read after releasing the lock.

func (*NodeSet) Bitmap added in v0.6.0

func (s *NodeSet) Bitmap() (bm *roaring64.Bitmap, shared bool)

Bitmap returns the set as a *roaring64.Bitmap. When the set is already in the bitmap state the live bitmap is returned (the caller must NOT mutate it); otherwise a fresh bitmap is materialised from the sorted ids. The materialised image is byte-identical under roaring's content-deterministic WriteTo to a bitmap that held the same ids all along, which is what keeps the label index's roaring-native on-disk format unchanged across this refactor (storage-engine-auditor, #1585).

shared reports whether the returned bitmap aliases the set's live bitmap (true only in the bitmap state); callers that need an independent copy must Clone when shared is true.

func (*NodeSet) Cardinality added in v0.6.0

func (s *NodeSet) Cardinality() uint64

Cardinality returns the number of NodeIDs in the set.

func (*NodeSet) Contains added in v0.6.0

func (s *NodeSet) Contains(node uint64) bool

Contains reports whether node is in the set. O(1) for the singleton state, O(log n) for the small array, and roaring's container probe for the bitmap state.

func (*NodeSet) IsEmpty added in v0.6.0

func (s *NodeSet) IsEmpty() bool

IsEmpty reports whether the set holds no NodeIDs.

func (*NodeSet) Minimum added in v0.6.0

func (s *NodeSet) Minimum() uint64

Minimum returns the smallest NodeID in the set. The caller must ensure the set is non-empty; on an empty set it returns 0.

func (*NodeSet) OrInto added in v0.6.0

func (s *NodeSet) OrInto(dst *roaring64.Bitmap)

OrInto adds every NodeID in the set to dst (set union into dst), preserving dst's ascending order. It is the allocation-light way to fold a small set into a destination bitmap during a range scan: a singleton becomes a single Add, a small set an AddMany of the sorted ids (which hits roaring's batch-by-high-bits fast path), and a bitmap a roaring Or — never materialising a throwaway bitmap for the inline states (graph-theory-expert, #1584).

func (*NodeSet) Remove added in v0.6.0

func (s *NodeSet) Remove(node uint64) (nowEmpty bool)

Remove deletes node from the set. No-op when absent. A NodeSet never demotes: removing from a bitmap leaves it a bitmap even if its cardinality drops to one (promote-and-never-demote, #1584). Returns true when the set became EMPTY as a result (so a caller maintaining a distinct-key count can drop the key).

func (*NodeSet) RemoveRange added in v0.6.0

func (s *NodeSet) RemoveRange(from, to uint64) (nowEmpty bool)

RemoveRange removes every id in [from, to] (inclusive). On an inline (non-bitmap) set it removes the few covered ids individually; on a bitmap it uses roaring's RemoveRange. A NodeSet never demotes, so a bitmap stays a bitmap. Returns true when the set became EMPTY.

func (*NodeSet) ToArray added in v0.6.0

func (s *NodeSet) ToArray() []uint64

ToArray returns the NodeIDs in strictly ascending order as a freshly allocated slice the caller owns. This is the canonical iteration order every index consumer relies on, and the exact sorted list the btree and hash on-disk formats serialize — so it is representation-independent.

type Serializer

type Serializer interface {
	Serialize(w io.Writer) error
	Deserialize(r io.Reader) error
}

Serializer is implemented by indexes that can persist and restore their internal state through an io.Writer / io.Reader pair. The Manager type-asserts every registered Subscriber to this interface during snapshot writes; subscribers that do not implement Serializer are silently skipped (rebuild-on-restart).

Implementations must:

  • Write a fixed self-describing header (magic + format version) so a future format bump can be detected on read.
  • Cover the entire on-disk payload with a CRC32C trailer (uint32 little-endian) so corruption surfaces as ErrIndexCorrupted.
  • Be safe for concurrent reads from other goroutines while Serialize executes (typically by holding the index's own RLock for the duration of the write).

Deserialize replaces the receiver's state with the contents of r. On any structural problem or CRC mismatch the function returns a wrapped ErrIndexCorrupted and leaves the receiver in its previous state.

type Subscriber

type Subscriber interface {
	Apply(Change)
	// Kind returns a short stable identifier of the underlying index
	// implementation, used for introspection (e.g. "label", "hash",
	// "btree").
	Kind() string
}

Subscriber is implemented by every concrete index that wishes to receive change events from the Manager. The Apply method must be idempotent: replays of the same change must not produce duplicate state.

Implementations must be safe for concurrent use: the Manager fans changes out to Apply while query goroutines read the same index concurrently, so a concrete index synchronises its own state internally (the built-in hash and label indexes hold an RWMutex). The Manager itself does not serialise an index's reads against its Apply calls.

Directories

Path Synopsis
Package btree provides an order-preserving property index over a constraints.Ordered value type, answering range predicates against the NodeIDs that carry each value.
Package btree provides an order-preserving property index over a constraints.Ordered value type, answering range predicates against the NodeIDs that carry each value.
Package hash provides a sharded hash index from arbitrary comparable property values to the set of NodeIDs that carry them, represented as a 64-bit Roaring bitmap.
Package hash provides a sharded hash index from arbitrary comparable property values to the set of NodeIDs that carry them, represented as a 64-bit Roaring bitmap.
Package label provides a Roaring-bitmap-backed inverted index from label identifiers to the NodeIDs that carry them.
Package label provides a Roaring-bitmap-backed inverted index from label identifiers to the NodeIDs that carry them.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL