Documentation
¶
Overview ¶
Package cypher provides the public query engine API for the GoGraph Cypher executor.
Usage ¶
g := lpg.New[string, float64](adjlist.Config{})
// ... populate graph ...
engine := cypher.NewEngine(g)
result, err := engine.Run(ctx, "MATCH (n) RETURN n", nil)
if err != nil { ... }
defer result.Close()
for result.Next() {
rec := result.Record()
_ = rec
}
Plan cache ¶
Engine caches parsed and translated logical plans together with the semantic-analysis verdict in a bounded LRU keyed by the query string. The cached entry is a *planCacheEntry; the physical build step runs per Engine.Run call so that per-call executor state is fresh. Semantically invalid queries are also cached (with the typed error) so that repeated runs of the same bad query short-circuit without re-parsing.
The default capacity is DefaultPlanCacheCapacity (1024 entries). Configure a different bound via EngineOptions.PlanCacheCapacity and the NewEngineWithOptions constructor. Eviction is least-recently-used and emits the cypher.plan_cache.evictions counter on the global metrics surface; hits and misses are reported under cypher.plan_cache.hits and cypher.plan_cache.misses.
Concurrency ¶
Engine is safe for concurrent use. Each Run call creates an independent physical operator tree. The plan cache itself serialises its structural updates on a single sync.Mutex; the cached *planCacheEntry is immutable once published, so callers operate on the returned pointer without further synchronisation.
Write queries serialise on a single-writer lock: the backing txn.Store's writer mutex when the engine is WAL-backed (NewEngineWithStore), or the engine's own writer mutex when it is store-less (NewEngine). Autocommit Engine.RunInTx holds that lock for one statement; an explicit transaction (Engine.BeginTx) holds it from BEGIN until COMMIT/ROLLBACK, so concurrent writers block until it finishes (write-write isolation). Reads (Engine.Run) never take the writer lock. The lock order is writer-lock (outermost) → the graph visibility barrier (visMu, inside lpg.Graph.ApplyAtomically); both wirings share it, so no deadlock is possible across them.
Transactions ¶
Engine.RunInTx is autocommit: each call is its own all-or-nothing, durable-then-visible transaction. Engine.BeginTx opens an explicit, multi-statement transaction (ExplicitTx) whose statements commit or roll back together — the engine substrate for the Bolt BEGIN/RUN/COMMIT/ROLLBACK protocol. Both apply writes eagerly to the in-memory graph and roll back via the in-memory undo log on error; a concurrent reader can therefore observe an open transaction's not-yet-committed writes (read-uncommitted for readers). See ExplicitTx (exectx.go) for the full transaction and isolation contract.
Index ¶
- Constants
- Variables
- func BindParams(params map[string]any) (map[string]expr.Value, error)
- func BuildPlan(plan ir.LogicalPlan, walker nodeWalkerIface, labelSrc labelResolverIface, ...) (op exec.Operator, cols []string, err error)
- func BuildPlanWithMutator(plan ir.LogicalPlan, walker nodeWalkerIface, labelSrc labelResolverIface, ...) (op exec.Operator, cols []string, err error)
- func QueryHasWritingClause(query string) bool
- func SuppressReorder(spine []ir.LogicalPlan) bool
- type ConstraintDef
- type Engine
- func NewEngine(g *lpg.Graph[string, float64]) *Engine
- func NewEngineWithOptions(g *lpg.Graph[string, float64], opts EngineOptions) *Engine
- func NewEngineWithRegistry(g *lpg.Graph[string, float64], reg expr.FunctionRegistry) *Engine
- func NewEngineWithStore(store *txn.Store[string, float64]) *Engine
- func NewEngineWithStoreAndConstraints(store *txn.Store[string, float64], recovered []recovery.ConstraintRecord) *Engine
- func NewEngineWithStoreAndSchema(store *txn.Store[string, float64], constraints []recovery.ConstraintRecord, ...) *Engine
- func (e *Engine) BeginReadTx(ctx context.Context) (*ExplicitTx, error)
- func (e *Engine) BeginTx(ctx context.Context) (*ExplicitTx, error)
- func (e *Engine) ClearPlanCache()
- func (e *Engine) ConstraintSpecsForSnapshot() []snapshot.ConstraintSpec
- func (e *Engine) Constraints() []ConstraintDef
- func (e *Engine) CountStoreCells() int
- func (e *Engine) Explain(query string, params map[string]expr.Value) (s string, err error)
- func (e *Engine) IndexSpecsForSnapshot() []snapshot.IndexDefSpec
- func (e *Engine) ListIndexes() []string
- func (e *Engine) Procs() *procs.Registry
- func (e *Engine) RefreshStatistics(ctx context.Context) error
- func (e *Engine) ResultRowCap() int64
- func (e *Engine) Run(ctx context.Context, query string, params map[string]expr.Value) (res *Result, err error)
- func (e *Engine) RunAny(ctx context.Context, query string, params map[string]any) (*Result, error)
- func (e *Engine) RunInTx(ctx context.Context, query string, params map[string]expr.Value) (res *Result, err error)
- func (e *Engine) RunInTxAny(ctx context.Context, query string, params map[string]any) (*Result, error)
- func (e *Engine) StatsTrackedPairs() int
- type EngineOptions
- type ErrStatementPipeline
- type ExplicitTx
- type IndexDef
- type Notification
- type Result
- func (r *Result) Close() error
- func (r *Result) Columns() []string
- func (r *Result) Err() error
- func (r *Result) IsClosed() bool
- func (r *Result) Next() bool
- func (r *Result) Notifications() []Notification
- func (r *Result) Record() exec.Record
- func (r *Result) RowAt(i int) []expr.Value
- func (r *Result) ValueAt(col int) expr.Value
Examples ¶
Constants ¶
const DefaultEdgeTypeFilterCacheCapacity = 256
DefaultEdgeTypeFilterCacheCapacity is the default upper bound on the number of entries held by an Engine's edge-type-filter cache. Chosen smaller than DefaultPlanCacheCapacity: the key space here is the set of distinct relationship-type combinations a workload's queries actually use (typically a handful — a schema has few relationship types), not raw query text, which churns far more under parameterisation and ad-hoc analytics.
Configure a different capacity via EngineOptions.EdgeTypeFilterCacheCapacity; pass 0 to use the default, or a positive integer to override. A negative value is rejected at construction time the same way PlanCacheCapacity is.
const DefaultMaxAggregateDistinctValues = 10_000_000
DefaultMaxAggregateDistinctValues is the default upper bound on the number of distinct values a [distinctAggregator] retains for one group before Step returns ErrAggregateDistinctMemoryExceeded. Matches the sibling pipeline-breaker caps' 10-million convention (exec.DefaultMaxDistinct, funcs.DefaultMaxCollectItems) high enough that ordinary queries — and the entire openCypher TCK — never reach it, yet finite so an unbounded count/sum/avg/min/max(DISTINCT …) fails fast instead of growing its seen-values set without limit. Not independently configurable via EngineOptions, exactly like exec.DefaultMaxDistinct/ exec.DefaultMaxGroups — only the buffering aggregators family (EngineOptions.MaxCollectItems) exposes a dedicated knob.
const DefaultMaxLabelRecountEdges = 4096
DefaultMaxLabelRecountEdges is the default per-relabel OUT-side recount ceiling for the relationship count-store (design §3.3). A relabel of a node with more out-edges than this dirties the OUT X-scoped cells rather than recounting them.
const DefaultMaxResultBytes int64 = 1 << 30 // 1 GiB
DefaultMaxResultBytes is the default upper bound on the aggregate estimated encoded size of the rows a single Engine.Run or Engine.RunInTx call materialises when EngineOptions.MaxResultBytes is left at its zero value. It is a coarse budget against the worst case the row cap alone cannot catch — a result that stays under DefaultMaxResultRows yet carries enough bytes per row to exhaust memory inside the visibility barrier. The default (1 GiB) is set high enough that ordinary queries, the openCypher TCK, and all examples stay well below it; callers that genuinely need an unbounded result size must opt out explicitly with MaxResultBytesUnlimited.
const DefaultMaxResultRows int64 = 10_000_000
DefaultMaxResultRows is the default upper bound on the number of rows a single Engine.Run or Engine.RunInTx call materialises when EngineOptions.MaxResultRows is left at its zero value. It bounds the worst case — an unintentional whole-graph scan or Cartesian product — so the engine never materialises an unbounded number of rows into memory inside the visibility barrier. It matches the sibling pipeline-breaker caps (exec.DefaultMaxSortRows, exec.DefaultMaxDistinct) and is set high enough that ordinary queries, the openCypher TCK, and all examples stay well below it; callers that genuinely need an unbounded result must opt out explicitly with MaxResultRowsUnlimited.
const DefaultParallelScanThreshold = 50_000
DefaultParallelScanThreshold is the default minimum live node count above which the planner prefers the morsel-parallel count reduce over the serial EagerAggregation pipeline (#1672). It is set well above one morsel (exec.DefaultMorselSize = 1024) so that the parallel path engages only when several morsels of work exist to spread across workers; below it the goroutine-spawn overhead would dominate and a small-query count stays serial and unaffected.
const DefaultPlanCacheCapacity = 1024
DefaultPlanCacheCapacity is the default upper bound on the number of entries held by an Engine's plan cache. Chosen so that a typical OLTP workload — the same hundreds of queries reissued by connection pools and ORMs — stays entirely in-cache without unbounded growth under high query-text churn (parameter-baked queries, ad-hoc analytics, fuzzed input).
Configure a different capacity via EngineOptions.PlanCacheCapacity; pass 0 to use the default, or a positive integer to override. A negative value is rejected at constructor time as a configuration error.
const GlobalMaxResultBytesUnlimited int64 = -1
GlobalMaxResultBytesUnlimited is the explicit opt-out sentinel for EngineOptions.GlobalMaxResultBytes: set the field to this value to disable the engine-wide aggregate ceiling. It is distinct from the zero value, which selects the GOMEMLIMIT-derived default (see EngineOptions.GlobalMaxResultBytes).
const MaxCollectItemsUnlimited = -1
MaxCollectItemsUnlimited is the explicit opt-out sentinel for EngineOptions.MaxCollectItems: set the field to this value to disable the per-group element budget entirely and allow an unbounded buffering aggregator (collect / percentile). It is distinct from the zero value, which selects funcs.DefaultMaxCollectItems. Use it only when memory is bounded by another means, because an unbounded `collect(n)` over a whole-graph scan then materialises every value into one list under the graph's visibility barrier.
const MaxLabelRecountEdgesUnlimited = -1
MaxLabelRecountEdgesUnlimited disables the per-relabel OUT-side recount ceiling when passed as EngineOptions.MaxLabelRecountEdges: the OUT side is always recounted exactly, whatever the node's out-degree.
const MaxResultBytesUnlimited int64 = -1
MaxResultBytesUnlimited is the explicit opt-out sentinel for EngineOptions.MaxResultBytes: set the field to this value to disable the aggregate-byte budget entirely. It is distinct from the zero value, which selects DefaultMaxResultBytes. Use it only when memory is bounded by another means, because an unbounded wide-row result then materialises every byte under the graph's visibility barrier.
const MaxResultRowsUnlimited int64 = -1
MaxResultRowsUnlimited is the explicit opt-out sentinel for EngineOptions.MaxResultRows: set the field to this value to disable the row cap entirely and allow an unbounded result. It is distinct from the zero value, which selects DefaultMaxResultRows. Use it only when the caller can bound memory by another means (e.g. streaming the result and closing it promptly), because an unbounded MATCH then materialises every row under the graph's visibility barrier.
Variables ¶
var ErrAggregateDistinctMemoryExceeded = errors.New("cypher: aggregate DISTINCT memory cap exceeded")
ErrAggregateDistinctMemoryExceeded is returned by [distinctAggregator.Step] once the number of distinct values seen for one group — or their estimated retained size — exceeds its configured bound. Matchable with errors.Is.
var ErrGlobalMemoryExceeded = errors.New("cypher: global result memory budget exceeded")
ErrGlobalMemoryExceeded is returned by Result.Err when materialising this result would push the engine-wide sum of concurrently-materialised result bytes over EngineOptions.GlobalMaxResultBytes. Where ErrResultBytesExceeded bounds ONE result, this bounds the AGGREGATE across all in-flight results on the engine — the residual memory-DoS the per-query cap alone cannot stop, in which N concurrent connections each materialise a per-query-capped result and their sum exhausts the host (#1842). It is a transient, load-dependent condition: the same query succeeds once other results close and free their charge, so it maps to a Neo.TransientError rather than a client error.
var ErrInternalPanic = errors.New("cypher: internal panic")
ErrInternalPanic wraps a recoverable panic that occurred while planning or executing a query on behalf of a single caller. The engine's query entrypoints (Engine.Run, Engine.RunInTx, Engine.RunAny, Engine.RunInTxAny) install a recover boundary so that such a panic — an index-out-of-range on a malformed plan, a nil dereference, a future bug — is converted into this error and returned to the caller instead of unwinding past the engine and crashing the embedding process. Callers may match it with errors.Is.
The returned error deliberately carries only the panic value, never a stack trace: the full trace (via runtime/debug.Stack) is logged to the default slog handler so internal details are not leaked to the caller. This is defence-in-depth against recoverable panics; a Go fatal runtime error (an uncatchable stack overflow) cannot be intercepted here and is instead prevented upstream by the parser's length/nesting guards.
var ErrParallelEdgeInSimpleGraph = errors.New("cypher: cannot create a parallel edge on a non-multigraph graph; construct the engine over a graph created with adjlist.Config{Multigraph: true}")
ErrParallelEdgeInSimpleGraph is returned by a Cypher write that would add a second relationship between an ordered node pair that already has one when the backing graph is not a multigraph and therefore cannot store the parallel edge. openCypher's data model is a multigraph in which every CREATE adds a relationship, so the Cypher engine must be constructed over a graph built with adjlist.Config{Multigraph: true}. The write fails fast and aborts the transaction rather than silently discarding the edge, upholding the module's fail-stop, never-fail-silent contract.
var ErrResultBytesExceeded = errors.New("cypher: result byte budget exceeded")
ErrResultBytesExceeded is returned by Result.Err when the cumulative estimated encoded size of the materialised rows exceeds EngineOptions.MaxResultBytes. It complements ErrResultRowsExceeded: the row cap bounds the *number* of rows, but a handful of rows carrying very large values (a node with megabyte-scale string properties) can dwarf a high row count, so the byte budget bounds that residual case. Like the row cap it is a permanent error tripped inside the visibility barrier during materialisation, before the surplus reaches the caller.
var ErrResultRowsExceeded = errors.New("cypher: result row limit exceeded")
ErrResultRowsExceeded is returned by Result.Next and Result.Err when the number of materialised rows exceeds EngineOptions.MaxResultRows. It is a permanent error: once set, subsequent Next calls return false.
var ErrTxFinished = errors.New("cypher: explicit transaction already finished")
ErrTxFinished is returned by ExplicitTx.Exec, ExplicitTx.Commit, and ExplicitTx.Rollback when the transaction has already been committed or rolled back. The handle holds no resources after it finishes — the writer serialisation is released and any WAL transaction is closed — so a stale call is rejected rather than acting on a released transaction. Matchable with errors.Is.
var ErrTxPoisoned = errors.New("cypher: transaction poisoned by a prior failed Exec statement — call Rollback")
ErrTxPoisoned is returned by ExplicitTx.Commit when a prior ExplicitTx.Exec call returned an ErrStatementPipeline error. A poisoned transaction cannot be committed — its partial writes must be unwound by calling ExplicitTx.Rollback instead. Matchable with errors.Is.
var ErrUndoFailed = errors.New("cypher: in-memory transaction undo failed; graph may be inconsistent until reopen")
ErrUndoFailed is returned when the in-memory transaction-undo replay itself fails — an inverse operation panicked while rolling back a write query's eager mutations. It is the in-memory analogue of txn.ErrCommittedNotApplied: it signals that the graph may be left in a state that neither fully contains nor fully excludes the failed transaction, so the inconsistency is surfaced to the caller (and counted via metrics) rather than silently ignored. A WAL-backed store reconciles to the durable state on the next reopen; the in-memory engine has no such backstop, so the caller must treat the graph as suspect. Callers may match it with errors.Is.
var ErrUnsupportedParamType = errors.New("cypher: unsupported parameter type")
ErrUnsupportedParamType is the sentinel wrapped by BindParams when a parameter value's Go type cannot be converted to an expr.Value (for example, a Bolt Point or temporal Struct sent as a raw parameter). It is a CLIENT fault — the request carried a value the engine cannot bind — so a front-end can classify it via errors.Is and map it to the appropriate client-error status (the Bolt server maps it to Neo.ClientError.Statement.TypeError). The wrapped message names only the offending Go type, which is the caller's own input and discloses nothing about internal server state.
var ErrWriteInReadOnlyTx = errors.New("cypher: write or DDL statement not allowed in a read-only transaction")
ErrWriteInReadOnlyTx is returned by ExplicitTx.Exec when a writing clause (CREATE/MERGE/SET/REMOVE/DELETE/DETACH) or a DDL statement (CREATE/DROP INDEX or CONSTRAINT) is issued inside a read-only explicit transaction opened with Engine.BeginReadTx. A read-only transaction holds neither the engine's writer serialisation, the visibility barrier, nor a WAL transaction, so a write has no lock, no barrier, and no durable log to record into; it is rejected BEFORE any execution so no state change can occur. Matchable with errors.Is.
Functions ¶
func BindParams ¶
BindParams converts a map[string]any to map[string]expr.Value using the following type mapping:
- nil → expr.Null
- bool → expr.BoolValue
- int, int8, int16, int32, int64 → expr.IntegerValue
- uint, uint8, uint16, uint32, uint64 → expr.IntegerValue (truncated to int64)
- float32, float64 → expr.FloatValue
- string → expr.StringValue
- []any → expr.ListValue (recursively converted)
- map[string]any → expr.MapValue (recursively converted)
- expr.Value → passed through unchanged
Returns an error wrapping ErrUnsupportedParamType for any value whose Go type is not in the list above.
Example ¶
ExampleBindParams converts a map of Go values into the engine's internal parameter representation. Engine.RunAny calls this for you; BindParams is exported for callers that bind once and run a query repeatedly.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/cypher"
)
func main() {
bound, err := cypher.BindParams(map[string]any{
"name": "acme",
"size": int64(42),
})
if err != nil {
fmt.Println("error:", err)
return
}
_, hasName := bound["name"]
_, hasSize := bound["size"]
fmt.Printf("bound=%d name=%v size=%v\n", len(bound), hasName, hasSize)
}
Output: bound=2 name=true size=true
func BuildPlan ¶
func BuildPlan( plan ir.LogicalPlan, walker nodeWalkerIface, labelSrc labelResolverIface, reg expr.FunctionRegistry, params map[string]expr.Value, ) (op exec.Operator, cols []string, err error)
BuildPlan converts an IR ir.LogicalPlan tree into a physical exec.Operator tree together with the ordered output column names.
walker provides node enumeration; labelSrc provides label-filtered scans; reg provides the built-in function registry; params are the query parameters.
Sprint 25 support matrix:
- ir.AllNodesScan
- ir.NodeByLabelScan
- ir.Selection (predicate is an always-true stub)
- ir.Projection
- ir.ProduceResults (required as root)
- ir.Expand (stub; child rows pass through, rel/dst vars bound to NULL)
func BuildPlanWithMutator ¶
func BuildPlanWithMutator( plan ir.LogicalPlan, walker nodeWalkerIface, labelSrc labelResolverIface, reg expr.FunctionRegistry, params map[string]expr.Value, mutator exec.GraphMutator, ) (op exec.Operator, cols []string, err error)
BuildPlanWithMutator converts an IR ir.LogicalPlan tree into a physical exec.Operator tree, supporting both read and write IR operators. The mutator provides the write surface for CREATE, SET, REMOVE, DELETE, and MERGE operators.
For read-only plans the behaviour is identical to BuildPlan; the mutator is only invoked when a write IR node is encountered.
func QueryHasWritingClause ¶
QueryHasWritingClause reports whether the query string contains any writing keyword (CREATE, MERGE, SET, REMOVE, DELETE, DETACH) outside a DDL prefix, i.e. whether it must be routed through Engine.RunInTx rather than Engine.Run. This is a textual heuristic: it avoids triggering the plan-cache machinery on a second pass, which would otherwise double-count hits and misses in concurrency tests.
External front-ends that classify queries as read vs write (for example, to serialise writers or pick a read replica) should call this rather than re-deriving the keyword set, so the classification stays in lockstep with Engine.RunAny.
The heuristic is intentionally permissive — false positives (writing keywords inside string literals or backtick identifiers) merely cause a read-only query to be routed through RunInTx, which executes identical semantics with the same correctness guarantees, only with the cost of opening and committing a write transaction.
func SuppressReorder ¶ added in v0.10.0
func SuppressReorder(spine []ir.LogicalPlan) bool
SuppressReorder reports whether a reorder at some point in the logical plan must be suppressed because an operator on its spine observes row order. The spine is the chain of ancestors of the reorder point, NEAREST ANCESTOR FIRST: spine[0] is the reorder point's immediate parent and spine[len-1] is the plan root. It returns true to suppress (keep the written order) and false when the reorder is order-safe. An empty spine (reorder at the root) is safe.
Types ¶
type ConstraintDef ¶ added in v0.2.0
type ConstraintDef struct {
// Label is the constrained node label.
Label string
// Property is the constrained property key.
Property string
// Name is the user-defined constraint name.
Name string
// Unique is true for a UNIQUE constraint, false for a NOT NULL constraint.
Unique bool
}
ConstraintDef is a durable constraint definition handed to the engine on open so it can re-register a constraint recovered from disk. It mirrors store/recovery.ConstraintRecord without coupling callers to the recovery package's wire types; ConstraintDefsFromRecovery converts a recovery result into this form.
func ConstraintDefsFromRecovery ¶ added in v0.2.0
func ConstraintDefsFromRecovery(recovered []recovery.ConstraintRecord) []ConstraintDef
ConstraintDefsFromRecovery converts the durable constraint set surfaced by store/recovery.Open into the ConstraintDef slice the engine constructor accepts via EngineOptions.RecoveredConstraints. Pass it the store/recovery.Result.Constraints field. The recovery package's wire kind (txn.ConstraintKind: 0 = UNIQUE, 1 = NOT NULL) is mapped to the boolean ConstraintDef.Unique.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the public query engine. It binds a graph, a function registry, and a plan cache, and exposes a single Run method for query execution.
Engine is safe for concurrent use. A single Engine may serve any number of concurrent Engine.Run readers together with concurrent Engine.RunInTx writers: each call builds its own operator tree, the plan cache is internally synchronised, and both the physical-plan build and execution run under the graph's visibility barrier (lpg.Graph.View for reads, lpg.Graph.ApplyAtomically for writes). A writer that grows the node space can therefore never tear a concurrent reader's plan build, and readers never observe a partially-applied write transaction (#1077, audit gap F3).
Write queries remain subject to the underlying store's single-writer constraint: when the Engine is backed by a txn.Store, concurrent Engine.RunInTx calls serialise on the store's writer mutex.
Write-path contract (secondary indexes) ¶
Secondary indexes the Engine maintains (via CREATE INDEX, and the backing indexes of UNIQUE constraints) are updated ONLY by writes that go through the Engine — Engine.RunInTx / Engine.RunAny. When an Engine maintains such indexes over a txn.Store, DO NOT also issue raw writes directly against the same store (txn.Tx AddNode / SetNodeLabel / SetNodeProperty / …): those bypass the Engine's index maintenance, so the secondary indexes go stale relative to the graph (an index-accelerated MATCH may miss nodes a label scan still finds) until the next restart, which rebuilds them. This is an intentional layering boundary — txn.Store is deliberately agnostic of Engine-maintained indexes. Pick one write path per graph: either drive all writes through the Engine, or use the raw txn.Store API on a graph without Engine-maintained indexes. The desync is never durable corruption (recovery rebuilds indexes from the WAL-correct graph), and it cannot occur for constraints/indexes declared through the store-direct DDL path (audit 2026-07-13 #1980).
func NewEngine ¶
NewEngine creates an Engine backed by g. The default built-in function registry (funcs.DefaultRegistry) and the default plan cache capacity (DefaultPlanCacheCapacity) are used. Use NewEngineWithOptions when a non-default function registry or plan cache capacity is required.
If g has no index.Manager attached yet, NewEngine installs a new empty one so that DDL statements (CREATE INDEX / DROP INDEX) work out of the box.
Example ¶
ExampleNewEngine shows the minimal setup: build an empty labelled property graph and bind it to an Engine ready to run queries.
package main
import (
"context"
"fmt"
"github.com/FlavioCFOliveira/GoGraph/cypher"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)
func main() {
g := lpg.New[string, float64](adjlist.Config{})
eng := cypher.NewEngine(g)
// A fresh engine over an empty graph runs queries that return no rows.
res, err := eng.Run(context.Background(), "MATCH (n) RETURN n", nil)
if err != nil {
fmt.Println("error:", err)
return
}
defer res.Close()
var rows int
for res.Next() {
rows++
}
fmt.Println("rows:", rows)
}
Output: rows: 0
func NewEngineWithOptions ¶
NewEngineWithOptions creates an Engine backed by g with explicit options. Zero-valued fields are filled with their documented defaults. When opts.Store is non-nil, the Engine is bound to that WAL-enabled txn.Store in addition to g.
If g has no index.Manager attached yet, a new empty one is installed.
func NewEngineWithRegistry ¶
NewEngineWithRegistry creates an Engine backed by g using a custom function registry and the default plan cache capacity.
If g has no index.Manager attached yet, a new empty one is installed.
func NewEngineWithStore ¶
NewEngineWithStore creates an Engine backed by a WAL-enabled txn.Store using the default plan cache capacity.
All write queries routed through Engine.RunInTx use a single txn.Tx for atomicity and WAL durability: mutations are applied eagerly to the in-memory graph (so reads within the same transaction see the writes) and the WAL is fsynced on Result.Close when no pipeline error occurred.
The underlying graph is taken from store.Graph(). If the graph has no index.Manager attached yet, a new empty one is installed.
Recovered constraints: when store was produced by opening a persisted database that had UNIQUE / NOT NULL constraints, this constructor AUTO-REGISTERS them from the graph's durable store-direct set so they are enforced (a duplicate or null is rejected) — enforcement is never silently disabled (#1981). Because the store-direct set does not retain the original user-defined constraint names, the auto-registered names are synthesised deterministically. For the ORIGINAL names, and to also re-register secondary INDEX definitions (which the store-direct set cannot reconstruct and which otherwise repopulate only from live-WAL index events), open with NewEngineWithStoreAndConstraints or NewEngineWithStoreAndSchema, passing the recovery result's schema. The engine logs a warning at construction when it auto-registers.
func NewEngineWithStoreAndConstraints ¶ added in v0.2.0
func NewEngineWithStoreAndConstraints(store *txn.Store[string, float64], recovered []recovery.ConstraintRecord) *Engine
NewEngineWithStoreAndConstraints creates a WAL-backed Engine that also re-registers the schema constraints recovered from disk. It is the recommended constructor for opening a persisted store: pass the store/recovery.Result.Constraints surfaced by the open that produced store, so a constraint declared before a crash is enforced again (audit gap H1). Using the plain NewEngineWithStore on a recovered store leaves the constraint registry empty and duplicates would be silently accepted.
recovered is converted via ConstraintDefsFromRecovery; pass nil (or use NewEngineWithStore) when there are no recovered constraints.
func NewEngineWithStoreAndSchema ¶ added in v0.3.0
func NewEngineWithStoreAndSchema(store *txn.Store[string, float64], constraints []recovery.ConstraintRecord, indexes []recovery.IndexRecord) *Engine
NewEngineWithStoreAndSchema creates a WAL-backed Engine that re-registers both the schema constraints and the secondary index definitions recovered from disk. It is the recommended constructor when opening a persisted store that may contain user-created indexes (task #1343). Pass the store/recovery.Result fields directly:
cypher.NewEngineWithStoreAndSchema(store, res.Constraints, res.Indexes)
Using NewEngineWithStoreAndConstraints on a store that also has indexes leaves the index manager populated only from the WAL-replayed index change events (not from the durable index definitions), so the planner's NodeByIndexSeek rewrites may serve an empty index immediately after restart. This constructor closes that gap by calling [registerRecoveredIndexes].
func (*Engine) BeginReadTx ¶ added in v0.4.0
func (e *Engine) BeginReadTx(ctx context.Context) (*ExplicitTx, error)
BeginReadTx opens a read-only explicit transaction bound to ctx. Unlike Engine.BeginTx, it acquires NO writer serialisation, opens NO WAL transaction, and does NOT hold the visibility barrier: a read-only transaction has no durability obligation and never serialises behind, or blocks, a concurrent writer. The caller MUST still finish the returned handle with exactly one ExplicitTx.Commit or ExplicitTx.Rollback; on a read-only handle both are teardown-only no-ops (they release nothing, since nothing was acquired).
Every statement run through ExplicitTx.Exec on the handle:
- is rejected with ErrWriteInReadOnlyTx BEFORE execution if it contains a writing clause (QueryHasWritingClause) or is DDL (ir.IsDDL) — the rejection is what keeps the lock-free read path safe, since a write would otherwise run with no writer lock, no barrier, and no WAL; and
- otherwise runs through the engine's concurrent read path (Engine.Run), taking its OWN per-statement lpg.Graph.View snapshot. Reads therefore observe READ-COMMITTED isolation across the statements of the transaction (each RUN sees the latest committed state, matching Neo4j's default), and run fully in parallel with other readers and writers.
If ctx is already cancelled or its deadline has elapsed, BeginReadTx returns promptly with an error wrapping the context error (matchable via errors.Is against context.Canceled / context.DeadlineExceeded).
func (*Engine) BeginTx ¶ added in v0.2.0
func (e *Engine) BeginTx(ctx context.Context) (*ExplicitTx, error)
BeginTx opens an explicit, multi-statement transaction bound to ctx and acquires the engine's writer serialisation: the store's single-writer mutex on a WAL-backed engine, or the engine writer mutex on a store-less engine. The caller MUST finish the returned handle with exactly one ExplicitTx.Commit or ExplicitTx.Rollback; until then the writer serialisation is held and concurrent writers block (write-write Isolation).
ctx bounds every statement executed through the handle. Pass the connection context (optionally narrowed with a transaction timeout) so that a cancelled connection, a server shutdown, or an elapsed timeout interrupts an in-flight statement and guarantees the writer serialisation cannot be held forever.
If ctx is already cancelled or its deadline has elapsed, BeginTx returns promptly without acquiring any lock, with an error wrapping the context error (matchable via errors.Is against context.Canceled / context.DeadlineExceeded).
See exectx.go for the full transaction and concurrency contract, including the read-committed isolation scope: concurrent readers block while this transaction is open and observe only the committed state once it ends (task #1412).
func (*Engine) ClearPlanCache ¶
func (e *Engine) ClearPlanCache()
ClearPlanCache drops every cached plan and increments the cypher.plan_cache.invalidations counter exactly once. It is the operator-facing invalidation hook installed on every DDL operator (CREATE/DROP INDEX, CREATE/DROP CONSTRAINT) — successful schema mutations call it so that subsequent queries re-plan against the new index / constraint topology rather than reusing stale plans built before the schema changed.
ClearPlanCache is also safe to invoke directly as a user-facing manual reset (e.g. from operational tooling after an out-of-band index swap on the underlying graph).
ClearPlanCache is idempotent and safe for concurrent use; each call emits exactly one invalidations counter increment regardless of the cache's prior size.
func (*Engine) ConstraintSpecsForSnapshot ¶ added in v0.2.0
func (e *Engine) ConstraintSpecsForSnapshot() []snapshot.ConstraintSpec
ConstraintSpecsForSnapshot converts the engine's current constraint set into the store/snapshot.ConstraintSpec slice that store/snapshot.WriteSnapshotFullWithConstraints (and its mapper-codec variant) persists into a snapshot's constraints.bin component. A checkpointer calls e.ConstraintSpecsForSnapshot() and hands the result to the writer so a checkpoint + WAL truncate does not lose constraints.
func (*Engine) Constraints ¶ added in v0.2.0
func (e *Engine) Constraints() []ConstraintDef
Constraints returns a structured snapshot of every schema constraint currently registered on the engine, in deterministic order (UNIQUE before NOT NULL, then by label, property, name). It is the source a checkpointer passes to store/snapshot.WriteSnapshotFullWithConstraints (via [ConstraintSpecsForSnapshot]) so the constraint set survives a checkpoint that truncates the WAL prefix which first declared a constraint (audit gap H1, the checkpoint-survival half).
Constraints is safe for concurrent use.
func (*Engine) CountStoreCells ¶ added in v0.10.0
CountStoreCells reports the number of distinct live relationship count-store cells the engine currently holds (design §2.3): the count-store's footprint, bounded by observed schema cardinality rather than by |V| or |E|. It is an observability accessor for the size indicator the metrics [Backend] cannot express as a gauge (task #2087); it returns 0 for an engine without a count store. Safe for concurrent use — it reads the store under its own shard read locks.
func (*Engine) Explain ¶
Explain returns a textual representation of the physical plan that would be chosen to execute query with the given params. The plan reflects current index availability: a hash index on the relevant (label, property) pair causes the relevant Selection+LabelScan subtree to appear as NodeByIndexSeek. No rows are produced; the graph is not modified.
The format mirrors ir.Explain but annotates Selection→LabelScan pairs that would be rewritten to index seeks at execution time.
Example ¶
ExampleEngine_Explain returns the physical plan for a query as text without executing it or touching the graph. Each row-producing operator is annotated with a cardinality estimate and its provenance tag (exact / stats / heuristic); the label scan over an empty graph is an exact count of zero (#2099).
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/cypher"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)
func main() {
g := lpg.New[string, float64](adjlist.Config{})
eng := cypher.NewEngine(g)
plan, err := eng.Explain("MATCH (n:Person) RETURN n", nil)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Print(plan)
}
Output: ProduceResults └─ Projection └─ NodeByLabelScan [n:Person] (est. rows=0, exact)
func (*Engine) IndexSpecsForSnapshot ¶ added in v0.6.0
func (e *Engine) IndexSpecsForSnapshot() []snapshot.IndexDefSpec
IndexSpecsForSnapshot converts the engine's current USER secondary-index definitions into the store/snapshot.IndexDefSpec slice that store/snapshot.WriteSnapshotFullWithConstraintsAndIndexDefs (and its mapper-codec variant) persists into a snapshot's indexdefs.bin component. A checkpointer calls e.IndexSpecsForSnapshot() and hands the result to the writer so a checkpoint + WAL truncate does not lose index definitions (#1755).
The set is sourced from the engine's own index-def registry (indexDefReg), not reconstructed from the index.Manager: the registry holds every user index def — including one whose backing subscriber is unbound after a recovery empty-graph fallback, which a Manager BoundNode() probe would miss — and never holds the internal numeric companion or a UNIQUE constraint backing index, so only the definitions recovery is expected to surface in res.Indexes are persisted.
func (*Engine) ListIndexes ¶ added in v0.3.0
ListIndexes returns the names of every secondary index currently registered on the engine, in unspecified order. The list reflects the live in-memory state: it includes every index created via CREATE INDEX and excludes any dropped via DROP INDEX. It is safe for concurrent use.
This is a thin wrapper over index.Manager.ListIndexes: the test suite and embedders that need to inspect the engine's schema without importing the graph/index package can use this method directly.
func (*Engine) Procs ¶
Procs returns the engine's procedure registry so callers can register custom procedures alongside the built-in db.* set. The returned *procs.Registry is the live, owning registry — mutations are observed immediately by every subsequent CALL <ns>.<name>() in any query parsed by this engine.
Returned registry is non-nil. Safe for concurrent use; see procs.Registry for the concurrency contract.
func (*Engine) RefreshStatistics ¶ added in v0.10.0
RefreshStatistics rebuilds the planner statistics for every (label, property) pair currently present on a live node, publishing a fresh snapshot atomically. It is the explicit maintenance entry point: statistics are best-effort and never maintained by a background goroutine, so a caller (a maintenance task, a scheduled job, or a test) drives the rebuild.
The scan runs under lpg.Graph.View, so it observes one consistent snapshot and does not block concurrent writers (which serialise elsewhere). Statistics built here ship INERT: no query-path consumer reads them yet (#2099 is the intended consumer), so a rebuild changes no plan. It honours context cancellation, returning ctx.Err() without publishing a partial snapshot.
func (*Engine) ResultRowCap ¶ added in v0.2.0
ResultRowCap reports the effective per-query result-row cap this Engine enforces, after EngineOptions.MaxResultRows has been resolved by the constructor:
- A positive value is the active cap. A single Engine.Run or Engine.RunInTx call materialising more than this many rows trips ErrResultRowsExceeded during the in-barrier drain, before the surplus rows are ever handed to the caller.
- Zero means the cap is disabled (the engine was built with MaxResultRowsUnlimited). Such an engine offers no upper bound on the rows a single query materialises, so an embedder exposing it to untrusted callers — for example behind the Bolt server — should bound memory by another means.
The accessor lets an embedder that receives a pre-built Engine observe its memory-safety posture without reaching into unexported state; the Bolt server uses it to warn when handed an uncapped engine.
func (*Engine) Run ¶
func (e *Engine) Run(ctx context.Context, query string, params map[string]expr.Value) (res *Result, err error)
Run parses, analyses, plans, and executes query, returning a materialised Result. The query is built and drained inside the read visibility barrier (Graph.View) so it observes a consistent, partial-transaction-free snapshot. DDL statements take a dedicated fast path. Parameters are bound from params and type-checked against the plan before execution.
If ctx is already cancelled or its deadline has elapsed when Run is called, it returns promptly — before any parse, plan, or execution work — with an error wrapping the context error (matchable via errors.Is against context.Canceled / context.DeadlineExceeded).
A recoverable panic raised while planning or executing the query is intercepted and returned as an error wrapping ErrInternalPanic; it never unwinds past this method to crash the embedding process.
Example ¶
ExampleEngine_Run runs a read query against a populated graph and reads a scalar aggregate from the streaming result. Result must always be closed.
package main
import (
"context"
"fmt"
"github.com/FlavioCFOliveira/GoGraph/cypher"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)
func main() {
g := lpg.New[string, float64](adjlist.Config{})
for _, key := range []string{"a", "b", "c"} {
if err := g.AddNode(key); err != nil {
fmt.Println("error:", err)
return
}
if err := g.SetNodeLabel(key, "Person"); err != nil {
fmt.Println("error:", err)
return
}
}
eng := cypher.NewEngine(g)
res, err := eng.Run(context.Background(), "MATCH (n:Person) RETURN count(n) AS people", nil)
if err != nil {
fmt.Println("error:", err)
return
}
defer res.Close()
for res.Next() {
rec := res.Record()
fmt.Println("people:", rec["people"])
}
}
Output: people: 3
func (*Engine) RunAny ¶
RunAny executes query with params expressed as map[string]any, automatically converting Go native types to expr.Value. See BindParams for the supported conversions.
RunAny auto-detects whether the query contains writing clauses (CREATE, MERGE, SET, REMOVE, DELETE, DETACH DELETE) and routes through Engine.RunInTx when so, or Engine.Run otherwise. Callers that need an explicit choice should invoke Engine.Run / Engine.RunInTx directly.
Example ¶
ExampleEngine_RunAny passes query parameters as a plain map[string]any, which the engine binds automatically. This is the convenient entry point for callers that do not want to import the internal value types.
package main
import (
"context"
"fmt"
"github.com/FlavioCFOliveira/GoGraph/cypher"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)
func main() {
g := lpg.New[string, float64](adjlist.Config{})
eng := cypher.NewEngine(g)
if _, err := drainTxAny(eng,
`CREATE (:Account {owner: "alice"}), (:Account {owner: "bob"})`); err != nil {
fmt.Println("seed error:", err)
return
}
// $owner is supplied as a Go string in the params map.
res, err := eng.RunAny(context.Background(),
`MATCH (a:Account) WHERE a.owner = $owner RETURN a.owner AS owner`,
map[string]any{"owner": "bob"},
)
if err != nil {
fmt.Println("error:", err)
return
}
defer res.Close()
for res.Next() {
fmt.Println("owner:", res.Record()["owner"])
}
}
// drainTxAny runs a write query in a transaction, draining and closing the
// result. It is a tiny helper shared by the parameter example above.
func drainTxAny(eng *cypher.Engine, query string) (int, error) {
res, err := eng.RunInTxAny(context.Background(), query, nil)
if err != nil {
return 0, err
}
defer res.Close()
var rows int
for res.Next() {
rows++
}
return rows, res.Err()
}
Output: owner: "bob"
func (*Engine) RunInTx ¶
func (e *Engine) RunInTx(ctx context.Context, query string, params map[string]expr.Value) (res *Result, err error)
RunInTx executes a write query against the engine's graph and returns a streaming Result. Unlike [Run], RunInTx inspects the IR plan for write operators; when any write operator is present it builds a mutator adapter so that write operators can modify the graph.
RunInTx is atomic: mutations apply eagerly to the live graph as the pipeline runs, with the inverse of each recorded into an in-memory undo log (see exectx.go's "Atomicity and the undo log" section for the shared mechanism). If the pipeline drain returns an error, a commit-time NOT NULL constraint is violated, the WAL fsync fails, or the pipeline panics, the whole statement rolls back: the undo log replays in reverse inside the write visibility barrier, restoring the graph to its pre-statement state, before the barrier is ever released — so a concurrent lpg.Graph.View reader can never observe a partially-applied write. Only once every check passes does the WAL fsync (durability before visibility), after which the undo log is discarded.
RunInTx is safe for concurrent use (each call creates an independent operator tree), subject to the single-writer constraint on write queries.
If ctx is already cancelled or its deadline has elapsed when RunInTx is called, it returns promptly — before any parse, plan, or txn.Store.Begin work — with an error wrapping the context error (matchable via errors.Is against context.Canceled / context.DeadlineExceeded).
Example ¶
ExampleEngine_RunInTx executes a CREATE inside a transaction (atomic and, for WAL-backed engines, durable) and then reads the data back in a second query.
package main
import (
"context"
"fmt"
"github.com/FlavioCFOliveira/GoGraph/cypher"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)
func main() {
g := lpg.New[string, float64](adjlist.Config{})
eng := cypher.NewEngine(g)
// Write: CREATE two labelled nodes atomically.
write, err := eng.RunInTx(context.Background(),
`CREATE (:Account {owner: "alice"}), (:Account {owner: "bob"})`, nil)
if err != nil {
fmt.Println("write error:", err)
return
}
for write.Next() { //nolint:revive // a write query streams no result rows; drain then close
}
if err := write.Err(); err != nil {
fmt.Println("write error:", err)
return
}
write.Close()
// Read-back: the committed nodes are visible to a subsequent query.
read, err := eng.Run(context.Background(), "MATCH (a:Account) RETURN count(a) AS accounts", nil)
if err != nil {
fmt.Println("read error:", err)
return
}
defer read.Close()
for read.Next() {
fmt.Println("accounts:", read.Record()["accounts"])
}
}
Output: accounts: 2
func (*Engine) RunInTxAny ¶
func (e *Engine) RunInTxAny(ctx context.Context, query string, params map[string]any) (*Result, error)
RunInTxAny executes a write query with params expressed as map[string]any, automatically converting Go native types to expr.Value. See BindParams.
func (*Engine) StatsTrackedPairs ¶ added in v0.10.0
StatsTrackedPairs reports the number of distinct (label, property) pairs the engine currently holds planner statistics for (task #2102): the statistics footprint, bounded by observed schema cardinality rather than by |V|. It is an observability accessor for the size indicator the metrics [Backend] cannot express as a gauge (mirroring Engine.CountStoreCells); it returns 0 for an engine that never refreshed statistics (the lazy collector is unallocated). Safe for concurrent use — it reads the collector's atomic-pointer snapshot.
type EngineOptions ¶
type EngineOptions struct {
// Registry, when non-nil, overrides the default built-in function
// registry used to resolve scalar function calls.
Registry expr.FunctionRegistry
// Store, when non-nil, binds the Engine to a WAL-enabled
// [txn.Store]. The Engine's graph is taken from store.Graph()
// when both Store and Graph fields are set; the explicit Graph
// is then ignored. Run queries through [Engine.RunInTx] for
// atomicity and WAL durability.
Store *txn.Store[string, float64]
// RecoveredConstraints, when non-empty, are the durable schema constraints
// recovered from disk (the [store/recovery.Result.Constraints] of the open
// that produced Store/Graph). The constructor re-registers each one in the
// engine's constraint registry and re-seeds every UNIQUE value-set by
// scanning the recovered graph, so a constraint declared before a crash is
// enforced again after recovery — without this, the registry is rebuilt
// empty on every open and duplicates are silently accepted (audit gap H1).
// A caller recovering a WAL-backed store from disk MUST pass these (or use
// [NewEngineWithStoreAndConstraints]); a store-less in-memory engine leaves
// the field nil.
RecoveredConstraints []ConstraintDef
// RecoveredIndexes, when non-empty, are the durable index definitions
// recovered from the WAL (the [store/recovery.Result.Indexes] of the open
// that produced Store/Graph). The constructor re-registers and re-backfills
// each one in the index.Manager so a user-created index survives a crash and
// a restart — without this, every CREATE INDEX is silently absent after
// recovery and the planner would serve 0-row results for indexed queries
// (audit gap: CREATE INDEX not durable). A caller recovering a WAL-backed
// store from disk MUST pass these (or use [NewEngineWithStoreAndSchema]);
// a store-less in-memory engine leaves the field nil.
RecoveredIndexes []IndexDef
// PlanCacheCapacity bounds the number of cached plans. Zero
// selects [DefaultPlanCacheCapacity]; positive values override
// it. A negative value is treated as misconfiguration and is
// clamped to the default by the constructor.
PlanCacheCapacity int
// EdgeTypeFilterCacheCapacity bounds the number of distinct
// relationship-type combinations whose filter map (rmp #1871) the
// Engine keeps cached. Zero selects
// [DefaultEdgeTypeFilterCacheCapacity]; positive values override it.
// A negative value is clamped to the default, mirroring
// PlanCacheCapacity.
EdgeTypeFilterCacheCapacity int
// MaxResultRows limits the number of rows a single [Engine.Run] or
// [Engine.RunInTx] call may materialise. If a query produces more rows than
// the limit, the [Result] iterator returns [ErrResultRowsExceeded] from
// [Result.Next] when the limit is hit, and [Result.Err] reports the same
// error.
//
// The value is interpreted as follows:
//
// - Zero (the default) selects [DefaultMaxResultRows], a finite cap that
// prevents an unintentional whole-graph scan or Cartesian-product query
// from materialising an unbounded number of rows — and holding the
// graph's visibility barrier — until memory is exhausted.
// - A positive value overrides the default. Set it to a value appropriate
// for the operational environment (e.g. 1_000_000 for a shared
// multi-tenant server).
// - [MaxResultRowsUnlimited] (-1) disables the cap entirely; use it only
// when memory is bounded by another means.
MaxResultRows int64
// MaxResultBytes is a coarse aggregate-BYTE budget on a single [Engine.Run]
// or [Engine.RunInTx] result, complementing [MaxResultRows]. The row cap
// bounds the number of rows; a small number of rows carrying very large
// values (a node with megabyte-scale string properties) can still consume
// large memory inside the visibility barrier under that cap. When the
// cumulative *estimated* encoded size of the materialised rows exceeds this
// budget, [Result.Err] reports [ErrResultBytesExceeded].
//
// The estimate is intentionally coarse and cheap (O(columns) per row, no
// allocation, no serialisation): a fixed per-value overhead plus the lengths
// of string/[]byte payloads and the element counts of lists/maps. It is a
// guard against pathological memory use, not an exact accounting of heap
// bytes.
//
// The value is interpreted as follows:
//
// - Zero (the default) selects [DefaultMaxResultBytes], a finite budget.
// - A positive value overrides the default (a byte count).
// - [MaxResultBytesUnlimited] (-1) disables the budget entirely; use it
// only when memory is bounded by another means.
MaxResultBytes int64
// GlobalMaxResultBytes is the engine-wide ceiling on the SUM of estimated
// bytes held by all concurrently-materialised results, complementing the
// per-result [MaxResultBytes]. The per-result cap bounds ONE query; this
// bounds the AGGREGATE across every connection sharing the engine, so N
// concurrent clients each running a per-query-capped-but-large result cannot
// sum to an out-of-memory condition (the residual load-dependent memory-DoS
// #1842). When materialising a result would push the engine-wide total over
// this ceiling, [Result.Err] reports [ErrGlobalMemoryExceeded] and that
// result serves no rows; results that close first free their charge, so the
// query succeeds on retry — hence a transient, not a permanent, error.
//
// The value is interpreted as follows:
//
// - Zero (the default) derives the ceiling from the Go soft memory limit:
// half of GOMEMLIMIT when the operator has set one (the recommended
// practice under memory pressure), else unlimited. This gives default-on
// protection sized to the operator's own declared budget, and never
// rejects a legitimate workload on a host whose total memory the module
// cannot know. Operators deploying under extreme concurrency should set
// GOMEMLIMIT (whereupon this activates automatically) or a positive value.
// - A positive value overrides the default (a byte count).
// - [GlobalMaxResultBytesUnlimited] (-1) disables the ceiling entirely.
GlobalMaxResultBytes int64
// MaxCollectItems bounds the number of values a single buffering aggregator
// — collect(), collect(DISTINCT …), percentileCont(), percentileDisc() —
// retains in one group. A grouping-key-free aggregate such as
// `RETURN collect(n)` forms exactly one group, so the group-count cap never
// fires; without this per-aggregator budget, `MATCH (n) RETURN collect(n)`
// would build an unbounded list inside the graph's visibility barrier.
//
// The value is interpreted as follows:
//
// - Zero (the default) selects [funcs.DefaultMaxCollectItems], a finite cap
// that prevents an unbounded collect/percentile buffer from exhausting
// memory and holding the visibility barrier.
// - A positive value overrides the default.
// - [MaxCollectItemsUnlimited] (-1) disables the cap entirely; use it only
// when memory is bounded by another means.
//
// When the budget is exceeded the aggregator returns
// [funcs.ErrCollectItemsExceeded], which the executor surfaces through
// [Result.Err] (the aggregation buffers during materialisation inside the
// barrier, so the cap trips before the whole list is built).
MaxCollectItems int
// ParallelScanThreshold is the minimum live node count above which the
// planner prefers the morsel-parallel count reduce over the serial
// EagerAggregation pipeline (#1672). Zero (the default) selects
// [DefaultParallelScanThreshold]. The gate is strict (>): a graph whose live
// node count is at or below the threshold always uses the serial path, so
// spawning workers only happens when there is enough work to amortise it.
// Ignored when [DisableParallelScan] is true.
ParallelScanThreshold int
// DisableHashJoin turns OFF the disconnected-equi-join hash-join physical
// optimisation (#1506). When false (the default) the planner may replace a
// nested-loop Cartesian product over two disconnected pattern parts joined
// by an equality predicate (`MATCH (a:A),(b:B) WHERE a.x = b.y …`) with an
// order-insensitive hash join, under the structural and order-safety guards
// in hash_join_plan.go. Setting it true forces the legacy nested-loop plan;
// it exists for the differential test that proves both plans return an
// identical result multiset, and as an operational escape hatch.
DisableHashJoin bool
// DisableRangeIndexSeek turns OFF the range-predicate B+tree index seek
// (#1505). When false (the default) the planner may replace a
// NodeByLabelScan+Filter for a range predicate (`n.p > x`, `n.p >= x`,
// `n.p < x`, `n.p <= x`, or a two-sided AND) on a property backed by a
// bound string btree index with a NodeByIndexRangeScan, under the
// comparability + selectivity guards in range_seek_plan.go. Setting it true
// forces the legacy scan+filter plan; it exists for the differential test
// that proves both plans return an identical result multiset, and as an
// operational escape hatch.
DisableRangeIndexSeek bool
// DisableMinLabelScan turns OFF the min-cardinality multi-label anchor scan
// (#2077). When false (the default) the planner anchors a multi-label node
// pattern (`MATCH (n:A:B) …`) on the smallest exact-cardinality label and
// re-checks the rest as a residual LabelPredicate Filter, instead of always
// anchoring on the first syntactic label. A label conjunction is commutative,
// so the substitution is result-identical and never scans more rows than the
// Labels[0] plan (min_i|Lᵢ| ≤ |Labels[0]|), with no statistics dependency —
// see min_label_scan_plan.go. Setting it true forces the legacy Labels[0]
// plan; it exists for the differential test that proves both plans return an
// identical result multiset, and as an operational escape hatch.
DisableMinLabelScan bool
// DisableJoinReorder turns OFF the count-store-gated disjoint-component
// ordering peephole (#2091). When false (the default) the planner may reorder
// a plain uncorrelated Apply that joins two disjoint single-scan components
// (`MATCH (a:A),(b:B) …`, a nested-loop Cartesian with no equi-join predicate)
// so the component with the smaller EXACT base cardinality drives, cutting the
// re-executions of the larger inner side. The swap changes only the emission
// order (a bag) and the internal column layout, never the multiset — see
// join_reorder_plan.go — and is admitted only when every base count is exact
// and the reorder is order-safe (SuppressReorder). Setting it true forces the
// written-order plan; it exists for the differential test that proves both
// plans return an identical result multiset, and as an operational escape
// hatch.
DisableJoinReorder bool
// DisableAnchorSwap turns OFF the count-store-gated single-edge anchor-swap
// peephole (#2090). When false (the default) the planner may re-root a
// single-edge pattern onto its other endpoint — flipping a written DirIn
// expand into a DirOut expand — when the count-store's exact D(label,relType,
// dir) degree statistics say that examines fewer edges (`MATCH (a:A)<-[:R]-(b:B)`
// re-rooted onto b). The swap is result-identical (it is the plan for the
// openCypher-mirror pattern; only emission order changes, proven unobserved by
// SuppressReorder) and, per the #2089a reverse-expand measurement, fires ONLY
// in the OUT-ward direction so it never introduces a reverse expand whose
// per-edge cost the aggregate counts cannot see — see anchor_swap_plan.go and
// docs/reordering-design.md §5.1. It is admitted only when every count is exact
// and non-dirty (a relabel-dirtied D cell vetoes) and the reorder is order-safe.
// Setting it true forces the written-order plan; it exists for the differential
// test that proves both plans return an identical result multiset, and as an
// operational escape hatch.
DisableAnchorSwap bool
// DisableParallelScan turns OFF the morsel-parallel count fast path (#1672).
// When false (the default) the planner serves a group-by-less count(*) /
// count(<scan-var>) over a bare full-node scan with a parallel reduce —
// summing per-worker partial counters over up to GOMAXPROCS worker
// goroutines — in place of the serial EagerAggregation pipeline, once the
// live node count exceeds [ParallelScanThreshold]. The parallel reduce
// produces a bit-identical count (int64 addition is associative and
// partition-invariant). Setting it true forces the serial path; it exists for
// the differential test that proves both plans return an identical result,
// and as an operational escape hatch. Small queries (at or below the
// threshold) always use the serial path regardless of this field, so they pay
// no goroutine-spawn cost. The full-node scan itself always runs serially: the
// morsel-parallel full-scan funnel was benchmarked as a regression and is not
// wired into the planner.
DisableParallelScan bool
// DisableParallelBackfill turns OFF the morsel-parallel phase-2 of a
// CREATE INDEX backfill ([Engine.backfillNodeHashIndex]). When false (the
// default) a backfill over at least backfillParallelMinNodes nodes is
// partitioned across a bounded worker pool (capped at GOMAXPROCS); setting it
// true forces the serial single-goroutine backfill. The two paths populate
// byte-identical index contents (insertion is set-semantic and
// order-independent), so this exists for the differential test that proves
// serial and parallel backfill agree, and as an operational escape hatch. It
// never changes durability or the registered index — only the backfill's
// internal concurrency.
DisableParallelBackfill bool
// MaxLabelRecountEdges bounds the per-relabel OUT-side fan-out the
// relationship count-store (#2082) recounts exactly when a node gains or
// loses a label (SET / REMOVE n:X). A relabel of a node with more than this
// many out-edges marks the affected OUT X-scoped D/T cells dirty (a veto to
// today's default plan, never a wrong exact) instead of recounting them,
// keeping per-commit work bounded on a hub relabel (design §3.3/§3.3.1). The
// IN side is always dirty-and-heal because GoGraph stores no reverse
// adjacency. E(relType) and N(label) are never dirty.
//
// - Zero (the default) selects [DefaultMaxLabelRecountEdges].
// - A positive value overrides it.
// - [MaxLabelRecountEdgesUnlimited] (-1) disables the ceiling, so the OUT
// side is always recounted exactly regardless of degree.
MaxLabelRecountEdges int
}
EngineOptions configures an Engine. The zero value is valid: it selects the default function registry (funcs.DefaultRegistry), no WAL-backed store, and the default plan cache capacity (DefaultPlanCacheCapacity). Use NewEngineWithOptions to construct an Engine from this struct.
EngineOptions is a plain configuration value read once by the constructor. It is safe for concurrent read use once constructed; do not mutate it after passing it to NewEngineWithOptions (the constructor copies the fields it needs, but the shared Registry and Store it references carry their own concurrency contracts).
type ErrStatementPipeline ¶ added in v0.3.0
type ErrStatementPipeline struct{ Err error }
ErrStatementPipeline wraps a runtime pipeline error from ExplicitTx.Exec. It signals that the query was compiled and ran to completion inside the visibility barrier but the execution pipeline failed (e.g. a constraint violation, a type error mid-pipeline, a validation error). The partial in-memory writes remain in the transaction's accumulated undo log; the caller (or the Bolt server layer) may decide whether to roll the whole transaction back.
Callers that need to distinguish pipeline errors from compile-time or build errors use errors.As to unwrap this type; the wrapped error is the original pipeline error (matchable via errors.Is against sentinel errors such as exec.ErrConstraintViolation).
func (*ErrStatementPipeline) Error ¶ added in v0.3.0
func (e *ErrStatementPipeline) Error() string
Error implements the error interface.
type ExplicitTx ¶ added in v0.2.0
type ExplicitTx struct {
// contains filtered or unexported fields
}
ExplicitTx is an open engine-level transaction spanning one or more statements. Obtain one from Engine.BeginTx; execute statements with ExplicitTx.Exec / ExplicitTx.ExecAny; finish with exactly one call to ExplicitTx.Commit or ExplicitTx.Rollback.
See the package file exectx.go for the full transaction, durability, and concurrency contract. In brief: writes accumulate and become durable together on Commit (WAL-backed) or unwind together on Rollback; the handle holds both the engine's writer serialisation and the graph's transaction-visibility write lock (visMu) for its whole lifetime — write-write Isolation for writers, and read-committed Isolation for concurrent readers (which block until the transaction ends); it is NOT safe for concurrent use by multiple goroutines.
func (*ExplicitTx) Commit ¶ added in v0.2.0
func (tx *ExplicitTx) Commit() (err error)
Commit makes the whole transaction durable and visible, then releases the writer serialisation. On a WAL-backed engine the WAL is fsynced exactly ONCE for every statement's accumulated writes (durable-then-visible, #1281) and the secondary-index buffer is committed; on a store-less engine the writes are already visible and Commit simply finalises the index buffer. The accumulated undo log is discarded. After Commit the handle is finished.
Commit runs the finalisation inside the visibility barrier so that, on a WAL-backed engine, the fsync happens-before the index commit and no concurrent reader can observe a committed-but-not-durable state. If the WAL fsync fails, the transaction is rolled back instead (in-memory undo replayed, index and WAL rolled back) and the fsync error is returned wrapping it: a transaction whose durability could not be guaranteed is reported as failed, never acknowledged.
Commit returns ErrTxFinished if the transaction was already committed or rolled back, and ErrTxPoisoned if a prior ExplicitTx.Exec call returned an ErrStatementPipeline error (call ExplicitTx.Rollback instead).
func (*ExplicitTx) Exec ¶ added in v0.2.0
Exec runs one statement inside the open transaction and returns a materialised Result. The statement's writes are applied eagerly and accumulate in the transaction; they are NOT made durable or finalised here — that happens once, at ExplicitTx.Commit. Closing the returned Result releases only its own iterator state; it never commits or rolls the transaction back.
A DDL statement (CREATE/DROP INDEX or CONSTRAINT) is rejected: schema changes are not transactional in this engine and must be issued outside an explicit transaction (autocommit). A read-only statement is permitted and simply observes the transaction's current state.
A statement that raises a runtime error is returned directly as the error return of Exec. The per-statement writes remain in the accumulated undo log, so the caller (the Bolt session) can roll the whole transaction back via ExplicitTx.Rollback after inspecting the error. A statement that panics is converted to an error wrapping ErrInternalPanic; the in-memory writes of the whole transaction are rolled back inside the visibility barrier, the writer serialisation is released, and the handle is marked finished (a subsequent Rollback is then a no-op).
Exec returns ErrTxFinished if the transaction has already been committed or rolled back, or if ctx (the BeginTx context) is already done.
func (*ExplicitTx) ExecAny ¶ added in v0.2.0
ExecAny is the ExplicitTx.Exec variant taking params as map[string]any, converting Go native values to expr.Value via BindParams.
func (*ExplicitTx) Rollback ¶ added in v0.2.0
func (tx *ExplicitTx) Rollback() (err error)
Rollback unwinds the whole transaction: it replays the accumulated in-memory undo log in reverse inside the visibility barrier (restoring the graph to its pre-transaction state), rolls back the secondary-index buffer, rolls back the WAL transaction (WAL-backed only, so a fresh recovery observes none of the writes), and releases the writer serialisation. After Rollback the handle is finished.
Rollback is best-effort and total: it always releases the writer serialisation and finishes the handle, even if an inverse operation fails. It returns ErrUndoFailed (wrapped) when the in-memory undo replay itself failed — the graph may then be inconsistent until reopen, which a WAL-backed engine reconciles to the durable state and a store-less engine cannot. It returns ErrTxFinished if the transaction was already committed or rolled back.
type IndexDef ¶ added in v0.3.0
type IndexDef struct {
// Name is the user-defined index name.
Name string
// Label is the indexed node label.
Label string
// Property is the indexed property key.
Property string
// Hash is true for a hash index, false for a btree index.
Hash bool
}
IndexDef is a durable index definition handed to the engine on open so it can re-register an index recovered from disk. It mirrors store/recovery.IndexRecord without coupling callers to the recovery package's wire types; IndexDefsFromRecovery converts a recovery result.
func IndexDefsFromRecovery ¶ added in v0.3.0
func IndexDefsFromRecovery(recovered []recovery.IndexRecord) []IndexDef
IndexDefsFromRecovery converts store/recovery.Result.Indexes into the engine's IndexDef slice so callers need not import the recovery package when constructing a NewEngineWithOptions with EngineOptions.RecoveredIndexes.
type Notification ¶ added in v0.3.1
type Notification struct {
// Code is the stable machine-readable notification code, e.g.
// "Neo.ClientNotification.Statement.CartesianProductWarning".
Code string
// Title is the short human-readable summary.
Title string
// Description is the full human-readable explanation, including any
// query-specific detail (such as the offending variable names).
Description string
// Severity is the advisory severity, e.g. "INFORMATION".
Severity string
// Category classifies the notification, e.g. "PERFORMANCE".
Category string
}
Notification is an out-of-band, advisory message attached to a query result. Notifications are NOT result rows and never affect the rows a query returns: they surface performance or correctness advisories (for example, a query that builds a Cartesian product between disconnected patterns) so a caller — an embedder of Engine or a Bolt driver via the SUCCESS metadata "notifications" field — can warn the user without changing query semantics.
The shape mirrors Neo4j's client notifications so a Bolt driver receives the same code/title/description it expects (#1483).
type Result ¶
type Result struct {
// contains filtered or unexported fields
}
Result is a forward-only streaming result set returned by Engine.Run / Engine.RunInTx. It wraps exec.ResultSet and exposes the same iterator contract.
Lifecycle contract ¶
Every Result returned from a successful Run/RunInTx call MUST be closed by the caller via Result.Close, even if Result.Err is non-nil and even if the caller stops iterating before exhaustion. Close releases the physical operator tree, drains any goroutines spawned by parallel operators, commits or rolls back buffered index mutations for write queries, and (for WAL-backed engines) fsyncs the WAL or rolls the transaction back.
The typical pattern is:
res, err := engine.Run(ctx, query, params)
if err != nil {
return err
}
defer res.Close()
for res.Next() {
rec := res.Record()
// ... consume rec ...
}
return res.Err()
Safety net ¶
Result installs a runtime.SetFinalizer that detects callers who forget to Close. When the garbage collector reclaims an unclosed Result, the finalizer:
- Increments the metric "cypher.result.leaked" so operators see the incidence count in their monitoring; and
- Best-effort closes the underlying resources to limit damage on a long-running server.
The finalizer is a fail-stop diagnostic, NOT a substitute for an explicit Close. In particular, the finalizer runs at an unpredictable time after the leak (it depends on the GC schedule) and CANNOT report errors back to the caller. For a write Result from Engine.RunInTx the WAL transaction is already committed (fsynced) or rolled back under the barrier before RunInTx returns (#1281), so the store's single-writer mutex is released at that point — a leaked, unclosed Result leaks only the ResultSet, not the write lock. Callers that need predictable resource release MUST still call Close themselves.
Result is NOT safe for concurrent use.
func NewErrResult ¶ added in v0.3.0
NewErrResult returns a zero-row Result whose Result.Err reports err immediately. It is intended for callers that need to represent a failed query as a Result rather than as an error return — for example, the Bolt server layer, which needs to store a per-statement error in a cursor so it surfaces at PULL time rather than at RUN time, preserving the Bolt v5 state-machine contract that a TX_STREAMING cursor is always drained before the session transitions back to TX_READY or FAILED.
Next always returns false and Err returns err.
func (*Result) Close ¶
Close releases all resources held by the result set.
For a write Result created by Engine.RunInTx, the buffered index changes and the WAL transaction were already committed (durably, fsync first) or rolled back inside the write query's lpg.Graph.ApplyAtomically window (commitUnderBarrier, #1281). Close therefore only releases the underlying ResultSet for such a result — the durability and visibility decision is made and finalised before RunInTx returns, never deferred to Close. The commit/rollback branches below survive only as a fallback for a Result that reached Close without that in-barrier finalisation (e.g. one that was never materialised), preserving the historical contract for that path.
Close is idempotent: a second invocation returns nil without re-entering the underlying ResultSet. The finalizer safety net also relies on this idempotence — see the type-level documentation.
func (*Result) Err ¶
Err returns the first error encountered during iteration, or nil.
When a bounded-resource guard truncated the result during materialisation, Err returns the guard's sentinel: ErrResultRowsExceeded when the row cap (EngineOptions.MaxResultRows) was hit, or ErrResultBytesExceeded when the aggregate-byte budget (EngineOptions.MaxResultBytes) was hit. Either is matchable with errors.Is. For an autocommit write statement a tripped guard is a failed statement: its eager mutations were rolled back atomically inside the barrier and nothing was made durable (#1338).
When the query was a write that failed and the subsequent in-memory undo replay ALSO failed (an inverse panicked, ErrUndoFailed), Err returns the pipeline error wrapped together with ErrUndoFailed so the caller learns both that the statement failed and that the rollback could not fully restore the graph; either is matchable with errors.Is.
When the in-barrier WAL fsync failed (#1281), Err returns that error. RunInTx already surfaces it directly and does not hand such a Result back, so this is a defensive backstop for any caller that nonetheless holds the Result: it reports that the write did not become durable (and was therefore rolled back).
func (*Result) Next ¶
Next advances to the next result row. Returns true when a row is available. If EngineOptions.MaxResultRows is set and the limit is reached, Next sets the result's error to ErrResultRowsExceeded and returns false.
func (*Result) Notifications ¶ added in v0.3.1
func (r *Result) Notifications() []Notification
Notifications returns the out-of-band advisories attached to this result by the planner — for example a Cartesian-product warning when the query builds a cross product between disconnected patterns (#1483). Notifications are NOT result rows and never affect iteration; a caller (or a Bolt driver via the SUCCESS "notifications" metadata) may surface them to warn the user. The returned slice is nil when the query produced no notifications.
func (*Result) Record ¶
Record returns the current row as a map from column name to value. Must only be called after a successful [Next].
For a materialised result the map is built lazily from the column-oriented backing store into a single reused scratch map (#1499): the returned map is owned by the Result and is overwritten on the next Record call, mirroring the streaming exec.ResultSet.Record contract. Callers that need to retain a row must copy it. Use Result.RowAt/Result.ValueAt to read values positionally without materialising the map.
func (*Result) RowAt ¶ added in v0.3.1
RowAt returns the materialised row at index i as a positional slice of values whose indices correspond to Result.Columns. The returned slice aliases the Result's backing store and must not be mutated or retained beyond the Result's lifetime. It is only valid for a materialised result (every read query and every RunInTx result is materialised under the visibility barrier); it panics if i is out of range. This is the allocation-free row accessor: it never builds the per-row map that Result.Record returns.
func (*Result) ValueAt ¶ added in v0.3.1
ValueAt returns the value at column index col of the current materialised row. It must only be called after a successful [Next] on a materialised result and is the allocation-free positional accessor used by hot consumers (the Bolt PULL path) that read every column by index and discard the row.
Source Files
¶
- anchor_swap_plan.go
- api.go
- constraint_check.go
- count_estimate.go
- count_maintenance.go
- count_metrics.go
- edge_type_filter_cache.go
- estimate.go
- exectx.go
- explain_estimate.go
- hash_join_plan.go
- index_binding.go
- join_reorder_plan.go
- min_label_scan_plan.go
- notification.go
- pattern_eval.go
- plan_cache.go
- range_seek_plan.go
- reorder_order_safety.go
- show.go
- stats_build.go
- stats_estimate.go
- stats_metrics.go
- stmt_now_reg.go
- subquery_eval.go
- undo.go
- undo_record.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package ast defines the Abstract Syntax Tree (AST) for openCypher 9.
|
Package ast defines the Abstract Syntax Tree (AST) for openCypher 9. |
|
Package exec implements the Volcano-style executor for the Cypher query engine.
|
Package exec implements the Volcano-style executor for the Cypher query engine. |
|
Package explain renders Cypher execution plans as human-readable text (EXPLAIN mode) and instruments them with per-operator execution statistics (PROFILE mode).
|
Package explain renders Cypher execution plans as human-readable text (EXPLAIN mode) and instruments them with per-operator execution statistics (PROFILE mode). |
|
Package expr defines the runtime value model for the Cypher executor.
|
Package expr defines the runtime value model for the Cypher executor. |
|
Package funcs implements the built-in Cypher function registry.
|
Package funcs implements the built-in Cypher function registry. |
|
Package ir defines the logical plan intermediate representation (IR) for the Cypher query compiler.
|
Package ir defines the logical plan intermediate representation (IR) for the Cypher query compiler. |
|
Package parser translates the ANTLR4-generated Cypher parse tree into the typed AST defined in github.com/FlavioCFOliveira/GoGraph/cypher/ast.
|
Package parser translates the ANTLR4-generated Cypher parse tree into the typed AST defined in github.com/FlavioCFOliveira/GoGraph/cypher/ast. |
|
gen
Package gen contains the ANTLR4-generated lexer and parser for openCypher 9.
|
Package gen contains the ANTLR4-generated lexer and parser for openCypher 9. |
|
Package procs defines the procedure registry for the Cypher executor.
|
Package procs defines the procedure registry for the Cypher executor. |
|
Package sema implements the scope-analysis pass for openCypher queries.
|
Package sema implements the scope-analysis pass for openCypher queries. |
|
Package tck records the conformance evolution of the GoGraph Cypher engine against the openCypher Technology Compatibility Kit.
|
Package tck records the conformance evolution of the GoGraph Cypher engine against the openCypher Technology Compatibility Kit. |