btree

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jun 17, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package btree provides an order-preserving property index over a constraints.Ordered value type, answering range predicates against the NodeIDs that carry each value.

The implementation is a cache-friendly in-memory B+ tree (task #1514): all (value, NodeID-set) data lives in the leaves, internal nodes hold separator keys + child pointers, and leaves are singly linked low→high for forward range scans. Insert and Delete of a distinct key are O(log n); point reads (Lookup, Cardinality) are O(log n); a range scan is O(log n + k) over the k keys it spans; and Index.BulkLoad builds the tree bottom-up in O(n) from sorted input. This replaces the original sorted-array index, whose per-key Insert and Delete were O(n) (an array shift) — the win is on write-heavy indexed workloads while every read path keeps its prior complexity. The tree internals live in bplus.go.

All operations are safe for concurrent use; a single sync.RWMutex guards the tree for the whole duration of each operation. Because the mutex fully excludes a writer's split/unlink from any in-flight reader, a reader can never observe a half-applied split or a dangling leaf. The mutex provides index-internal isolation only; transaction isolation across multiple calls is the engine's responsibility.

Key ordering

Keys are ordered by the TOTAL order of cmp.Compare / cmp.Less, not by the raw < operator. The two orders agree everywhere except IEEE 754 NaN: under the total order a floating-point NaN key is less than every other value (including math.Inf(-1)), every NaN bit pattern compares equal to every other NaN, and ±0.0 are one key. Raw < is only a partial order over floats — every comparison with NaN is false — so a single NaN insert used to break the monotone predicate that sort.Search requires and silently corrupted the index for ordinary keys (task #1354). With the total order the sorted invariant holds for every representable input: NaN is a regular, deduplicated key that Lookup/Delete address and that no range with a non-NaN lower bound ever returns.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrMismatchedLengths = errors.New("btree: values and nodes slices must have the same length")

ErrMismatchedLengths is returned by Index.BulkLoad when the values and nodes slices supplied to it do not share a common length. Before sprint 21 this condition panicked; the error returned here lets callers handle it as a recoverable input validation failure.

Functions

This section is empty.

Types

type Binding added in v0.3.1

type Binding[V cmp.Ordered] struct {
	// PropertyID is the interned property-key identifier this index covers.
	// Property changes whose Change.Property differs are ignored.
	PropertyID uint32

	// LabelID is the interned label identifier this index is scoped to.
	// Label changes whose Change.Label differs are ignored.
	LabelID uint32

	// Label and Property are the source names behind PropertyID and LabelID.
	// They let a query planner match the index against a (label, property)
	// predicate without access to the registries.
	Label, Property string

	// Project converts a Change.OldValue / Change.NewValue payload to the
	// index key type. ok is false when the payload is absent or not indexable
	// (wrong kind), in which case the event is skipped for that direction.
	Project func(v any) (V, bool)

	// Eligible reports whether the node should currently be present in the
	// index: it must be live (not deleted) and carry the bound label,
	// evaluated against the graph's final state.
	Eligible func(node graph.NodeID) bool

	// CurrentValue returns the node's current value for the bound property,
	// projected to the key type. ok is false when the node is not live, lacks
	// the property, or the value is not indexable. It is consulted on label
	// add/remove events, which carry no property payload.
	CurrentValue func(node graph.NodeID) (V, bool)
}

Binding ties an Index to a single (label, property) pair of a live node graph. The shape mirrors hash.Binding so the engine's CREATE INDEX wiring can build either kind from the same closures. Because changes are fanned out at commit time — after the transaction's mutations were applied eagerly to the graph — the callbacks observe the transaction's FINAL state, which is exactly the state the index must converge to.

type Index

type Index[V cmp.Ordered] struct {
	// contains filtered or unexported fields
}

Index is an order-preserving property index keyed by V, backed by an in-memory B+ tree (see bplus.go).

Example

ExampleIndex shows an order-preserving property index keyed by an ordered value type: insert (value, NodeID) pairs, then read back the NodeIDs carrying one exact value and the half-open count of distinct values.

package main

import (
	"fmt"

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

func main() {
	// An index over an integer "age" property.
	idx := btree.New[int]()
	idx.Insert(30, graph.NodeID(1))
	idx.Insert(25, graph.NodeID(2))
	idx.Insert(30, graph.NodeID(3)) // same value, different node

	// Lookup returns the NodeID set carrying exactly age == 30.
	bm := idx.Lookup(30)
	fmt.Println("age==30 nodes:", bm.ToArray())
	fmt.Println("age==30 cardinality:", idx.Cardinality(30))
	fmt.Println("distinct ages:", idx.DistinctValues())
}
Output:
age==30 nodes: [1 3]
age==30 cardinality: 2
distinct ages: 2

func New

func New[V cmp.Ordered]() *Index[V]

New returns an empty index.

func NewBound added in v0.3.1

func NewBound[V cmp.Ordered](b Binding[V]) (*Index[V], error)

NewBound returns an empty B+tree index bound to b. Unlike New, the returned index has a functional Index.Apply: it subscribes to the node property and label changes selected by b and keeps itself consistent with the graph. Returns an error when b is missing its Label, Property, or any of the three callbacks.

func (*Index[V]) Apply

func (i *Index[V]) Apply(c index.Change)

Apply maintains a bound index (see NewBound) from the index.Manager change fan-out; it is a no-op for an unbound index (see New), which cannot reliably interpret arbitrary index.Change values without the caller-supplied binding (property key + value-type coercion). The bound rules live in [Index.applyBound] (bound.go).

func (*Index[V]) BoundNode added in v0.3.1

func (i *Index[V]) BoundNode() (label, property string, ok bool)

BoundNode returns the (label, property) pair this index is bound to, with ok reporting whether the index is bound at all. Query planners use it to decide whether the index may serve a predicate: a bound index covers exactly its (label, property) pair, while an unbound index carries no coverage metadata and must NOT be served by a range seek (it is never maintained from the fan-out).

func (*Index[V]) BulkLoad

func (i *Index[V]) BulkLoad(values []V, nodes []graph.NodeID) error

BulkLoad replaces the contents of the index with the given (value, node) pairs in O(n log n) time. The pairs slice is left untouched. Calling BulkLoad on a non-empty index discards previous data. Returns ErrMismatchedLengths when len(values) != len(nodes). Values are sorted and deduplicated under the total order described in the package documentation, so NaN inputs collapse into one leading entry instead of corrupting (or, before task #1354, hanging) the load.

func (*Index[V]) Cardinality

func (i *Index[V]) Cardinality(value V) uint64

Cardinality returns the number of NodeIDs associated with value, matched under the total order (see Index.Lookup).

func (*Index[V]) Delete

func (i *Index[V]) Delete(value V, node graph.NodeID)

Delete removes node from the set associated with value. No-op when absent. The (value, bitmap) entry is removed when its bitmap becomes empty, and a leaf that becomes entirely empty is unlinked (see the delete policy in bplus.go). Like Index.Insert, value is matched under the total order, so Delete addresses a NaN-keyed entry.

func (*Index[V]) Deserialize

func (i *Index[V]) Deserialize(r io.Reader) error

Deserialize replaces the receiver's state with the contents of r. Because the writer dumps entries in ascending key order, the reader can build the sorted entries slice directly without an extra sort pass; the loader is therefore O(n) instead of Index.BulkLoad's O(n log n).

Keys must be STRICTLY ascending under the cmp.Compare total order — the only shape Index.Serialize produces. A payload that violates it fails fail-stop with index.ErrIndexCorrupted rather than load an index whose binary searches would silently miss live keys. In particular, a float64 payload written before the total-order fix (task #1354) that carries a NaN key after a real key, or duplicate NaN entries, is rejected; the index is derived data, so the caller recovers by rebuilding it from the primary graph. A single NaN entry in the leading position is the legitimate post-fix encoding and loads normally.

func (*Index[V]) DistinctValues

func (i *Index[V]) DistinctValues() int

DistinctValues returns the number of distinct values currently indexed. It is O(1): the tree maintains a running key count.

func (*Index[V]) Insert

func (i *Index[V]) Insert(value V, node graph.NodeID)

Insert records that node carries value. Keys follow the total order described in the package documentation, so a floating-point NaN is a valid key: it sorts before every other value and all NaN bit patterns share one entry. Inserting a new distinct key is O(log n).

func (*Index[V]) Kind

func (*Index[V]) Kind() string

Kind returns "btree" — satisfies index.Subscriber.

func (*Index[V]) Lookup

func (i *Index[V]) Lookup(value V) *roaring64.Bitmap

Lookup returns a clone of the bitmap associated with value, or an empty bitmap when value is unknown. Matching uses the total order, so Lookup(NaN) returns the NaN entry when one exists.

func (*Index[V]) Range

func (i *Index[V]) Range(lo, hi V) *roaring64.Bitmap

Range returns a Roaring bitmap that is the union of the per-value bitmaps for every key v with lo <= v <= hi under the total order. The returned bitmap is freshly allocated; the caller owns it. A NaN key is below every other value, so any range with a non-NaN lo — including Range(math.Inf(-1), math.Inf(1)) — never returns it.

Example

ExampleIndex_Range shows an inclusive range predicate [lo, hi]: Range returns every NodeID whose value satisfies lo <= v <= hi, and RangeFirst returns the smallest matching value together with one of its NodeIDs (the order-preserving property the index exists for).

package main

import (
	"fmt"

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

func main() {
	idx := btree.New[int]()
	if err := idx.BulkLoad(
		[]int{10, 20, 30, 40},
		[]graph.NodeID{1, 2, 3, 4},
	); err != nil {
		fmt.Println("bulk load:", err)
		return
	}

	// Range is inclusive on both ends: [20, 40] selects 20, 30 and 40.
	bm := idx.Range(20, 40)
	fmt.Println("nodes in [20,40]:", bm.ToArray())

	v, node, ok := idx.RangeFirst(20, 40)
	fmt.Printf("first in [20,40]: value=%d node=%d ok=%t\n", v, node, ok)
}
Output:
nodes in [20,40]: [2 3 4]
first in [20,40]: value=20 node=2 ok=true

func (*Index[V]) RangeCount added in v0.3.1

func (i *Index[V]) RangeCount(lo, hi V, budget uint64) (count uint64, exact bool)

RangeCount returns the exact number of NodeIDs whose value falls within the inclusive interval [lo, hi] under the total order, but stops accumulating as soon as the running total exceeds budget and returns (budget+1, false) — the caller learns only that the count is "more than budget" without paying to walk the whole range. When the full count is ≤ budget it is returned with exact == true.

The entries are pairwise-disjoint node-sets (each node carries exactly one value for the property), so the sum of per-entry cardinalities equals the union cardinality exactly, with no allocation and no union materialisation (graph-theory-expert, #1505). The early-exit bounds the gate cost to O(budget) cardinality probes regardless of how many distinct values the range spans, which keeps a non-selective range cheap to reject.

func (*Index[V]) RangeFirst

func (i *Index[V]) RangeFirst(lo, hi V) (V, graph.NodeID, bool)

RangeFirst returns the first NodeID in the smallest indexed value not less than lo and not greater than hi, plus that value. The second return value reports whether any match exists. It is the allocation-free way to peek the first row of a range scan; the full union of matches is available via Index.Range. Bounds compare under the total order, so lo = NaN admits a NaN key while any non-NaN lo excludes it.

func (*Index[V]) Serialize

func (i *Index[V]) Serialize(w io.Writer) error

Serialize writes every (value, NodeID-set) pair in key order to w. The on-disk layout is:

uint32 magic ('SBTR')
uint32 formatVersion
uint64 entryCount
repeat entryCount times:
  uint32 keyLen
  [keyLen]byte key (kind-specific encoding)
  uint64 idCount
  [idCount]uint64 NodeIDs (sorted ascending)
uint32 crc32c (little-endian)

Writing in key order lets [Deserialize] use Index.BulkLoad indirectly: the reader appends one entry at a time and the sorted order is preserved.

Jump to

Keyboard shortcuts

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