query

package
v0.14.0 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package query provides a fluent, type-safe programmatic API for expressing MATCH-style pattern queries against a labelled property graph snapshot.

The API is intentionally minimal in v1: it covers the high-value "MATCH (n:Label1) WHERE n.prop = v RETURN n" pattern and its single-hop extension "(:Label1)-[]->(:Label2)". Multi-hop chains compose via repeated Pattern.Out / [Pattern.Filter] calls; the engine transparently uses the lpg.Graph's NodeIndex (Roaring bitmaps) when a WithLabel predicate seeds the pattern.

A future iteration will plug in graph/index.Manager so the planner can choose between hash, btree, and full-scan plans based on cardinality estimates.

Concurrency

An Engine is read-only and takes no lock across pattern steps, so it is safe for concurrent use by multiple goroutines only while the underlying lpg.Graph and CSR snapshot are quiescent (no concurrent mutation); this is the same quiescence the CSR snapshot already requires. A Pattern is a single MATCH expression under construction: it is mutated in place by each builder call (Pattern.Vertex, Pattern.Out) and is NOT safe for concurrent use — a Pattern is owned by one goroutine.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Engine

type Engine[N comparable, W any] struct {
	// contains filtered or unexported fields
}

Engine bundles an lpg.Graph with its CSR snapshot for read-only query execution. The CSR is used for adjacency traversal; the LPG is used for label / property lookups.

Removed nodes are invisible: every working-set construction step (seeding, label intersection, Pattern.Out expansion) prunes NodeIDs that lpg.Graph.IsTombstoned reports as removed, so Pattern.Cardinality, Pattern.Collect, and Pattern.NodeIDs never observe deleted state. Each pattern step reads the tombstone set at the moment it executes; the Engine takes no lock across steps, so callers that mutate the graph concurrently with query construction must serialise externally (the same quiescence the CSR snapshot already requires).

func New

func New[N comparable, W any](g *lpg.Graph[N, W], c *csr.CSR[W]) *Engine[N, W]

New returns an Engine wrapping g and the CSR snapshot c.

func (*Engine[N, W]) Match

func (e *Engine[N, W]) Match() *Pattern[N, W]

Match opens a new MATCH expression seeded with every live node in the graph: interned NodeIDs only (never the ghost slots the sharded id packing leaves in [0, MaxNodeID)), minus the tombstoned set.

Example

ExampleEngine_Match expresses "MATCH (n:Person) RETURN n" with the fluent pattern API. The label predicate seeds the working set from the graph's label index; Cardinality reports its size and Collect returns the matching user keys (order unspecified, sorted here).

package main

import (
	"fmt"
	"sort"

	"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
	"github.com/FlavioCFOliveira/GoGraph/graph/csr"
	"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
	"github.com/FlavioCFOliveira/GoGraph/graph/query"
)

// buildSocialGraph returns a tiny labelled property graph and its CSR
// snapshot: two :Person nodes (alice, bob) who both BOUGHT a :Product
// (widget).
func buildSocialGraph() (*lpg.Graph[string, int], *csr.CSR[int]) {
	g := lpg.New[string, int](adjlist.Config{Directed: true})
	for _, p := range []string{"alice", "bob"} {
		_ = g.AddNode(p)
		_ = g.SetNodeLabel(p, "Person")
		_ = g.SetNodeProperty(p, "name", lpg.StringValue(p))
	}
	_ = g.AddNode("widget")
	_ = g.SetNodeLabel("widget", "Product")
	_ = g.AddEdge("alice", "widget", 0)
	_ = g.AddEdge("bob", "widget", 0)
	return g, csr.BuildFromAdjList(g.AdjList())
}

func main() {
	g, snap := buildSocialGraph()
	eng := query.New(g, snap)

	people := eng.Match().Vertex(query.WithLabel[string, int]("Person"))

	keys := people.Collect()
	sort.Strings(keys)
	fmt.Println("count:", people.Cardinality())
	fmt.Println("people:", keys)
}
Output:
count: 2
people: [alice bob]

type Pattern

type Pattern[N comparable, W any] struct {
	// contains filtered or unexported fields
}

Pattern is a single MATCH expression under construction. Its working set (the current NodeID bitmap) is mutated in place by each builder call (Pattern.Vertex, Pattern.Out), so a Pattern is NOT safe for concurrent use: a Pattern is owned by one goroutine.

func (*Pattern[N, W]) Cardinality

func (p *Pattern[N, W]) Cardinality() uint64

Cardinality returns the size of the current working set.

func (*Pattern[N, W]) Collect

func (p *Pattern[N, W]) Collect() []N

Collect returns the user-facing N values in the working set.

func (*Pattern[N, W]) NodeIDs

func (p *Pattern[N, W]) NodeIDs() iter.Seq[graph.NodeID]

NodeIDs returns an iterator over the NodeIDs in the working set.

func (*Pattern[N, W]) Out

func (p *Pattern[N, W]) Out() *Pattern[N, W]

Out expands the working set to the out-neighbours of every node in it. Neighbours that have been tombstoned since the CSR snapshot was built are pruned: the snapshot still stores their incident edges, but a removed node must never re-enter the working set.

Example

ExamplePattern_Out follows out-edges one hop — "MATCH (:Person)-[]->(p) RETURN p" — collapsing both buyers onto the single product they bought.

package main

import (
	"fmt"

	"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
	"github.com/FlavioCFOliveira/GoGraph/graph/csr"
	"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
	"github.com/FlavioCFOliveira/GoGraph/graph/query"
)

// buildSocialGraph returns a tiny labelled property graph and its CSR
// snapshot: two :Person nodes (alice, bob) who both BOUGHT a :Product
// (widget).
func buildSocialGraph() (*lpg.Graph[string, int], *csr.CSR[int]) {
	g := lpg.New[string, int](adjlist.Config{Directed: true})
	for _, p := range []string{"alice", "bob"} {
		_ = g.AddNode(p)
		_ = g.SetNodeLabel(p, "Person")
		_ = g.SetNodeProperty(p, "name", lpg.StringValue(p))
	}
	_ = g.AddNode("widget")
	_ = g.SetNodeLabel("widget", "Product")
	_ = g.AddEdge("alice", "widget", 0)
	_ = g.AddEdge("bob", "widget", 0)
	return g, csr.BuildFromAdjList(g.AdjList())
}

func main() {
	g, snap := buildSocialGraph()
	eng := query.New(g, snap)

	bought := eng.Match().
		Vertex(query.WithLabel[string, int]("Person")).
		Out()

	fmt.Println("count:", bought.Cardinality())
	fmt.Println("products:", bought.Collect())
}
Output:
count: 1
products: [widget]

func (*Pattern[N, W]) Vertex

func (p *Pattern[N, W]) Vertex(preds ...Predicate[N, W]) *Pattern[N, W]

Vertex constrains the working set by the conjunction of preds. The first call with a WithLabel predicate uses the LPG's label index (Roaring intersect) for the planner's fast path; subsequent calls fall back to a per-node scan.

type Predicate

type Predicate[N comparable, W any] interface {
	Match(g *lpg.Graph[N, W], id graph.NodeID) bool
}

Predicate is the type-safe interface a Vertex constraint implements. Implementations may consult the lpg.Graph freely; returning true keeps the NodeID in the working set.

func WithLabel

func WithLabel[N comparable, W any](name string) Predicate[N, W]

WithLabel returns a Predicate selecting nodes carrying the given label.

func WithProperty

func WithProperty[N comparable, W any](key string, expected lpg.PropertyValue) Predicate[N, W]

WithProperty returns a Predicate selecting nodes whose named property is EQUAL to the given expected value.

Equality semantics

The relation is openCypher EQUALITY, not comparability and not equivalence:

  • INTEGER and FLOAT are one numeric kind, compared EXACTLY. An lpg.Int64Value of 5 is equal to an lpg.Float64Value of 5, and an int64 is never widened to float64, so 4611686018427387905 and 4611686018427387900 stay distinct even though both round to 2^62.
  • A NaN expected value is equal to nothing, and nothing is equal to a NaN-valued property — including another NaN. Equality is the relation in which NaN = NaN is FALSE; under equivalence it is true, and that other relation is not what this predicate implements.
  • STRING, BOOLEAN, BYTES and TIME are equal within their own kind. Note that openCypher's equatability is WIDER than its comparability, so for these kinds this predicate matches where the degenerate WithRange [v, v] does not: they are equatable but not ordered scalars.
  • Every other cross-kind pair is not equal. INTEGER x FLOAT is the sole off-diagonal entry openCypher unifies, so a number is never equal to a string.

Index acceleration

When the owning graph carries an index covering the predicate's (label, property) pair — and the same Pattern.Vertex call also constrains that label — the engine narrows the working set from the index before any property is read. A STRING or BOOLEAN equality is served EXACTLY from a bound hash index of that key type and the per-node comparison is skipped; a NUMERIC equality is served as a SUPERSET from the float64-keyed numeric btree companion and the exact comparison still runs over what the seek left. Either way the answer is identical to the answer the same query returns with no index present (see index_seek.go).

Example

ExampleWithProperty filters a pattern by an exact property match — "MATCH (n) WHERE n.name = 'alice' RETURN n".

package main

import (
	"fmt"

	"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
	"github.com/FlavioCFOliveira/GoGraph/graph/csr"
	"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
	"github.com/FlavioCFOliveira/GoGraph/graph/query"
)

// buildSocialGraph returns a tiny labelled property graph and its CSR
// snapshot: two :Person nodes (alice, bob) who both BOUGHT a :Product
// (widget).
func buildSocialGraph() (*lpg.Graph[string, int], *csr.CSR[int]) {
	g := lpg.New[string, int](adjlist.Config{Directed: true})
	for _, p := range []string{"alice", "bob"} {
		_ = g.AddNode(p)
		_ = g.SetNodeLabel(p, "Person")
		_ = g.SetNodeProperty(p, "name", lpg.StringValue(p))
	}
	_ = g.AddNode("widget")
	_ = g.SetNodeLabel("widget", "Product")
	_ = g.AddEdge("alice", "widget", 0)
	_ = g.AddEdge("bob", "widget", 0)
	return g, csr.BuildFromAdjList(g.AdjList())
}

func main() {
	g, snap := buildSocialGraph()
	eng := query.New(g, snap)

	match := eng.Match().Vertex(
		query.WithProperty[string, int]("name", lpg.StringValue("alice")),
	)

	fmt.Println("count:", match.Cardinality())
	fmt.Println("keys:", match.Collect())
}
Output:
count: 1
keys: [alice]

func WithRange added in v0.6.0

func WithRange[N comparable, W any](key string, lo, hi lpg.PropertyValue) Predicate[N, W]

WithRange returns a Predicate selecting nodes whose named property lies in the inclusive interval [lo, hi].

Comparison semantics

The two bound tests — v >= lo and v <= hi — are INDEPENDENT, exactly as openCypher's comparability rules make them, so lo and hi need not share a kind. A bound test holds only when the two values are comparable AND the relation is true:

  • STRING against STRING: byte-wise order.
  • INTEGER against INTEGER, FLOAT against FLOAT, and INTEGER against FLOAT: one numeric order, compared EXACTLY. An int64 is never widened to float64, so 4611686018427387905 and 4611686018427387900 stay distinct even though both round to the same float64 (2^62).
  • Every other pair — a number against a string, or either side a stored lpg.PropBool, lpg.PropBytes, lpg.PropTime or lpg.PropList — is not comparable, and its bound test is false. (A Cypher temporal value is delivered to the property layer as a TAGGED PropString, not as a PropTime, so it is ordered as a string; that is unchanged by #2600.)

A NaN operand makes every comparison FALSE (never null), which is the IEEE-754 outcome openCypher adopts: a NaN property value never matches, and a NaN bound matches nothing.

Index acceleration

When the owning graph carries a btree index covering the predicate's (label, property) pair — and the same Pattern.Vertex call also constrains that label — the engine narrows the working set from the index with one O(log n + k) seek before any property is read. What happens next depends on whether that seek is exact:

  • A STRING range is served EXACTLY, so the per-node comparison is skipped entirely and no property is read.
  • A NUMERIC range is served from a float64-keyed index whose keys round above 2^53, so the seek is only a SUPERSET: the exact comparison above still runs, but over what the seek left rather than over the whole working set.

Either way the answer is identical to the answer the same query returns with no index present.

Jump to

Keyboard shortcuts

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