cypher

package
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 41 Imported by: 0

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 DO NOT serialise. Concurrency control is MVCC and nothing else (rmp #2306): independent writers run concurrently on both wirings, and a write-write conflict between them is DETECTED at commit — surfaced as a serialization conflict — rather than prevented by holding a lock. The two locks that used to serialise them, the engine's own writer mutex and the backing txn.Store's capacity-one semaphore, are gone.

What remains, and what each excludes:

  • Engine.schemaMu — an RWMutex a DDL statement (CREATE/DROP INDEX or CONSTRAINT) takes EXCLUSIVELY and an ordinary write takes SHARED. It makes a DDL's validate-then-register sequence atomic against concurrent writes, which is what keeps a constraint from being registered over data that violates it. It does not order two ordinary writers against each other.
  • the graph schema barrier (visMu) — taken SHARED inside lpg.Graph.ApplyVersioned by every ordinary write since rmp #2320, and exclusively only by DDL and an explicit transaction.

Reads (Engine.Run) take neither. The lock order is schemaMu (outermost) → the store's writer admission → visMu; 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

Examples

Constants

View Source
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.

View Source
const DefaultGlobalMaxResultBytes int64 = 4 << 30 // 4 GiB

DefaultGlobalMaxResultBytes is the engine-wide result-byte ceiling applied when EngineOptions.GlobalMaxResultBytes is left at zero AND the process has no Go soft memory limit to derive one from.

The per-query budget is finite by default (DefaultMaxResultBytes, 1 GiB), but a per-query bound says nothing about the sum: N concurrent clients each staying inside their own 1 GiB may still materialise N GiB, and the engine-wide ceiling is the only bound that governs that. Because the GOMEMLIMIT derivation above yields nothing when no memory limit is set — the Go runtime's default state — that ceiling was absent in the commonest deployment, so the aggregate was bounded only by the concurrency the caller admitted. Under the extreme concurrency this module targets, the aggregate is the bound that matters.

The value is 4 GiB: 4x the per-query default, so a workload that legitimately runs several large results concurrently is unaffected, while a fleet of concurrent large-result queries is bounded. A deployment that sets GOMEMLIMIT keeps the derived half and is unaffected; GlobalMaxResultBytesUnlimited remains the explicit opt-out.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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).

View Source
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.

View Source
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.

View Source
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.

View Source
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

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

func BindParams(params map[string]any) (map[string]expr.Value, error)

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:

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

func QueryHasWritingClause(query string) bool

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 the physical-plan build and execution of a WRITE run under lpg.Graph.ApplyVersioned, which holds the schema barrier shared (rmp #2320). A reader takes a snapshot and no lock at all (rmp #2290). 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 — the second guarantee now coming from the transaction's shared commit record rather than from exclusion (#1077, audit gap F3).

Write queries remain subject to the underlying store's writer-admission 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

func NewEngine(g *lpg.Graph[string, float64]) *Engine

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

func NewEngineWithOptions(g *lpg.Graph[string, float64], opts EngineOptions) *Engine

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

func NewEngineWithRegistry(g *lpg.Graph[string, float64], reg expr.FunctionRegistry) *Engine

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

func NewEngineWithStore(store *txn.Store[string, float64]) *Engine

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 snapshot (lpg.Graph.BeginRead). 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. It acquires NOTHING: no writer serialisation, no visibility barrier, no lock of any kind. Concurrency control is MVCC alone. The caller MUST finish the returned handle with exactly one ExplicitTx.Commit or ExplicitTx.Rollback.

This doc used to read "but it does take the graph's visibility barrier exclusively, which until rmp #2305 retires it still blocks concurrent writers for the transaction's lifetime". THAT IS FALSE and has been since rmp #2305 did retire it — the sentence outlived the change it was describing, and it contradicted both this file's own header ("What serialises an explicit transaction: NOTHING") and the [ExplicitTx.view] field doc. What keeps this handle's reads stable is the transaction id it stamps its versions with, not exclusion (rmp #2345).

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. It no longer guards against a serialisation being held forever — there is none to hold — but it still bounds a statement queued behind a DDL.

ctx also bounds the ACQUISITION, not only the statements that follow. BeginTx takes three things in order — the writer serialisation, the WAL transaction, and the visibility barrier — and every one of them honours ctx, so a caller whose deadline elapses while queued behind another writer or behind an in-flight reader gets the context error rather than a transaction it is no longer entitled to. When that happens NOTHING is left held and no handle is returned. Before rmp #2174 two of the three acquisitions ignored ctx entirely: the round-3 audit measured a 50 ms deadline returning after 601 ms, and after 11.60 s under load, both times with err=nil and a live transaction, which also made the Bolt tx_timeout inert at BEGIN.

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).

The error is returned within the deadline plus a small, bounded margin: the margin is one scheduling hop, not the holder's remaining tenure. See mvcc.Gate.StrongLockCtx and the acquireCtx helper beside it for why a queued lock acquisition cannot simply be abandoned and what is done instead.

See exectx.go for the full transaction and concurrency contract, including the isolation scope: concurrent readers do NOT block while this transaction is open, and observe the state before it began until it commits (task #1412, strengthened by rmp #2290).

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) Close added in v0.11.0

func (e *Engine) Close() error

Close releases the background resources the engine's graph owns — currently the MVCC vacuum goroutine — and waits for them to terminate. It is the io.Closer the engine's owner calls at shutdown.

Why the engine has this at all (rmp #2308)

The graph owns a goroutine now: reclamation moved off the commit path onto a demand-started background vacuum. That vacuum exits on its own once there is nothing left to reclaim, so an owner that never closes leaks nothing — but an owner that wants the goroutine gone at a KNOWN instant had no way to say so, because the engine holds the only reference to its graph. `internal/sim`'s metrics oracle found the gap immediately: it certifies that a workload returns to its exact goroutine baseline with zero slack, and it had no teardown to call.

It closes the GRAPH and nothing else. The engine's own state — the plan cache, the registries, the statistics collector — spawns no goroutine and needs no teardown, and the durable store's pieces belong to [store.DB.Close], which the embedder owns separately.

Idempotent and safe to call concurrently with any other operation. The engine stays usable afterwards; what stops is reclamation, so an owner that closes and then keeps writing accumulates versions with nothing to release them.

func (*Engine) CloseCtx added in v0.11.0

func (e *Engine) CloseCtx(ctx context.Context) error

CloseCtx is Engine.Close with a deadline on the join; see lpg.Graph.CloseCtx for what the context does and does not bound.

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

func (e *Engine) CountStoreCells() int

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

func (e *Engine) Explain(query string, params map[string]expr.Value) (s string, err error)

Explain returns a textual representation of the plan that executes query with the given params. No rows are produced and the graph is not modified.

For a READING statement the rendering is the PHYSICAL plan: the operator tree the builder actually produced, walked node by node, with each node named after its concrete operator type. Hash-join substitution, columnar and parallel tier engagement and the chosen access path are therefore visible, and cannot disagree with what runs — a HashJoin is named HashJoin because it IS one.

Before rmp #2222 this rendered the logical IR instead, and was wrong in BOTH directions: it reported NodeByIndexSeek where a label scan ran, and printed CartesianProduct for

MATCH (a:P), (b:P) WHERE a.age = b.age RETURN count(*)

which the runtime counter proves executes as a HashJoin — showing a reader O(n·m) where O(n+m) runs.

For a WRITING statement it renders the logical plan and says so on the first line. A write's physical tree cannot be built without a live mutator (the operators bind to an open transaction), so there is nothing faithful to walk outside one; the honest label is preferable to silently returning a different kind of plan. Use Engine.Profile on a read to inspect a physical tree with measurements.

Example

ExampleEngine_Explain returns the PHYSICAL plan for a query as text without executing it or touching the graph: the operator tree the builder actually produced, each node named after its concrete operator type. Because the name comes from the operator itself, the rendering cannot disagree with what runs — an index seek appears as NodeByIndexSeek only when a NodeByIndexSeek is what was built.

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:
Project
└─ NodeByLabelScan [Person]

func (*Engine) ExplainLogical added in v0.11.0

func (e *Engine) ExplainLogical(query string, params map[string]expr.Value) (s string, err error)

ExplainLogical returns the LOGICAL plan for query, annotated with the index seeks and the count-store-gated reorderings the read path would apply, and with each operator's estimated row count.

It is the companion to Engine.Explain, not a lesser version of it, and the two answer different questions:

  • Explain answers "what runs?" — the physical operator tree, named after the operators themselves, so it cannot misreport the access path, the join, or the tier.
  • ExplainLogical answers "what did the planner think?" — the logical shape plus the CARDINALITY ESTIMATES that drove the physical choices. Those estimates belong to the logical nodes and have no counterpart on a built operator, so they are only visible here.

Reach for this one when an estimate looks wrong (a plan chosen on a bad guess); reach for Explain when you need to know what actually executes.

Example

ExampleEngine_ExplainLogical returns the LOGICAL plan, which is where the planner's cardinality ESTIMATES live: each row-producing operator carries an estimate and its provenance tag (exact / stats / heuristic), and the label scan over an empty graph is an exact count of zero (#2099). Those estimates have no counterpart on a built operator, so they are visible only here — use this to understand why a plan was chosen, and cypher.Engine.Explain to see what runs.

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.ExplainLogical("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

func (e *Engine) ListIndexes() []string

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) NewSession added in v0.11.0

func (e *Engine) NewSession() *Session

NewSession returns a Session over this engine.

It is cheap — no locks, no registration, no resources to release — so a server may mint one per connection and discard it when the connection ends.

Safe for concurrent use.

func (*Engine) Procs

func (e *Engine) Procs() *procs.Registry

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) Profile added in v0.11.0

func (e *Engine) Profile(ctx context.Context, query string, params map[string]expr.Value) (s string, err error)

Profile executes query with the given params and returns the PHYSICAL plan annotated with each operator's emitted row count and the wall-clock time attributed to it — GoGraph's equivalent of the PROFILE both incumbents expose (Neo4j adds db-hits, Memgraph relative time).

The query really runs: rows are produced and discarded, so Profile is for a reading statement only and returns an error for a writing one rather than performing its writes as a side effect of a diagnostic.

Times are INCLUSIVE of an operator's children, because a pipelined operator's Next pulls from them. Subtract a node's children to obtain its exclusive cost — the same arithmetic a reader of Neo4j's PROFILE performs.

Profiling is off unless this method is called: the instrumentation is a wrapper installed by the builder, so an ordinary Engine.Run executes code identical to a build in which profiling does not exist (rmp #2222 AC 3).

func (*Engine) RefreshStatistics added in v0.10.0

func (e *Engine) RefreshStatistics(ctx context.Context) error

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 resolves against one pinned snapshot, so it observes a consistent instant and does not block concurrent writers (which serialise elsewhere). It takes no barrier — see the note on the internal builder below for why wrapping it in the old lpg.Graph.View would not have given the property it claimed.

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) RefreshStatisticsLocked added in v0.11.0

func (e *Engine) RefreshStatisticsLocked(ctx context.Context) error

RefreshStatisticsLocked is Engine.RefreshStatistics for a caller that ALREADY holds the visibility barrier — specifically db.stats.refresh(), which runs inside query execution (#2196).

It exists because visMu is a non-re-entrant sync.RWMutex: taking it again from a goroutine already inside Graph.View would DEADLOCK the engine. The re-entrancy guard turns that into a panic, but only in a debug or race build — a production binary would hang. So the barrier-taking and barrier-free entry points must be distinct, and the caller has to pick correctly.

Correctness is unchanged: the scan only reads, and the caller's read barrier already pins the consistent snapshot it needs.

func (*Engine) ResultRowCap added in v0.2.0

func (e *Engine) ResultRowCap() int64

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) (*Result, error)

Run parses, analyses, plans, and executes query, returning a materialised Result. The query is built and drained at ONE read instant — a snapshot opened here and released when the statement finishes — so it observes a consistent, partial-transaction-free view. 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

func (e *Engine) RunAny(ctx context.Context, query string, params map[string]any) (*Result, error)

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 snapshot 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 per-operator-tree single-goroutine 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

func (e *Engine) StatsTrackedPairs() int

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

	// DisableCSRPairCache turns off the Engine's cross-query forward/reverse CSR
	// pair reuse (rmp #2143). The cache is ON by default because it removes an
	// O(V+E) rebuild from every query that traverses — measured 52% faster and 96%
	// less allocated memory on a warm, selective one-hop over 960k edges.
	//
	// Disable it when an Engine is long-lived over a large graph and retention
	// matters more than latency: the cache keeps one CSR pair reachable for the
	// Engine's lifetime, roughly (V+1)*8 + E*24 bytes per direction, which no
	// result-memory ceiling bounds. A write-heavy workload also gets little from it,
	// since every topology change invalidates the entry — watch
	// cypher.csr_pair_cache.replacements against .hits to tell.
	DisableCSRPairCache bool

	// 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

	// DisablePrefixIndexSeek turns OFF the STARTS WITH prefix range seek (#2127)
	// while leaving the rest of the range seek in place. When false (the default)
	// the planner rewrites `n.p STARTS WITH 'x'` on a property backed by a bound
	// string btree index into a NodeByIndexRangeScan over [x, succ(x)) — a prefix
	// IS a range under the byte-lexicographic order the btree is laid out in — with
	// the original predicate retained as the residual Filter, under the same
	// exact-count selectivity gate the other range predicates use.
	//
	// It is a SEPARATE knob from DisableRangeIndexSeek, not a reuse of it, so the
	// differential test can toggle the prefix rewrite ALONE and keep the `>=`/`<`
	// seek active in both arms — two arms that differ in exactly one variable.
	// Setting it true forces the legacy scan+filter plan for a prefix predicate; it
	// also serves as an operational escape hatch.
	DisablePrefixIndexSeek bool

	// DisableBitmapIntersection turns OFF the set-at-a-time multi-label
	// conjunction (#2133). When false (the default) the planner answers
	// `MATCH (n:A:B)` by intersecting the labels' Roaring bitmaps — one k-way AND
	// under a single index read-lock — and drops the residual label Filter, which
	// the intersected bitmap subsumes. Gated on the EXACT intersection cardinality
	// (roaring64.AndCardinality, which allocates nothing), so the decision needs no
	// new statistic; when it vetoes, control falls through to the min-label anchor
	// scan below, never to something worse.
	//
	// It is a SEPARATE knob from DisableMinLabelScan so the differential test can
	// toggle the intersection alone and keep the shipped min-label plan active in
	// both arms — two arms differing in exactly one variable. Setting it true forces
	// the scan-and-filter plan; it is also an operational escape hatch.
	DisableBitmapIntersection 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

	// DisableExpandIntoSeek turns OFF the O(log d) seek for a hop whose destination
	// variable is already bound — cycle closing, triangles, mutual-relationship
	// detection (#2149). When false (the default) such a hop binary-searches the
	// bound destination's contiguous run in the destination-ordered CSR and walks
	// only the matching slots; when true it keeps the #2206 expand-into FILTER and
	// walks the source's whole neighbour run, paying the edge-type filter's map
	// lookup and the cyphermorphism check on every slot.
	//
	// The seek is result-identical AND order-identical — the slots sharing a
	// destination are contiguous and handle-ordered, so the seeked block is exactly
	// the subsequence the filter would have emitted, in the same order — so this
	// knob changes performance only. It exists for the differential test that proves
	// the two agree row for row, and as an operational escape hatch. See
	// docs/design-expand-into-symmetric-swap.md §3, §4.
	DisableExpandIntoSeek bool

	// EnableCyclicIntersect turns ON the fused cyclic expand (#2157): for a pattern
	// that closes a cycle, the open middle hop and the closing seek are replaced by
	// one [exec.ExpandIntersect] driven by a sorted-set intersection over ordered CSR
	// runs, so a candidate that does not close the cycle is never built into a row.
	//
	// The polarity is POSITIVE — unlike every Disable* knob here — so the zero value
	// leaves the operator OFF. That is deliberate and is what SPIKE #2155 required:
	// the operator is new rather than a peephole, the openCypher TCK contains no
	// directed cycle over three or more distinct node variables and therefore cannot
	// gate it at all, and the recogniser changes which operators a plan is built
	// from. Opting in explicitly keeps every existing plan byte-identical until the
	// benchmark task has measured each qualifying shape.
	//
	// Result- and order-identical when on: see docs/design-wcoj-cyclic-patterns.md.
	EnableCyclicIntersect 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.

func (*ErrStatementPipeline) Unwrap added in v0.3.0

func (e *ErrStatementPipeline) Unwrap() error

Unwrap returns the underlying pipeline error so errors.Is and errors.As traversal works correctly.

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 NO LOCK across its lifetime — each statement takes the schema barrier SHARED for its own duration and nothing is held between statements (rmp #2305), and the engine's former writer serialisation (Engine.writeMu) no longer exists at all (rmp #2306). Write-write isolation therefore comes from per-object conflict detection against this transaction's snapshot (rmp #2300), not from exclusion. A concurrent reader takes no barrier and observes snapshot isolation against the state before this transaction began, without waiting for it (rmp #2290); the handle itself 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

func (tx *ExplicitTx) Exec(query string, params map[string]expr.Value) (res *Result, err error)

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

func (tx *ExplicitTx) ExecAny(query string, params map[string]any) (*Result, error)

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:

  1. Increments the metric "cypher.result.leaked" so operators see the incidence count in their monitoring; and
  2. 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 writer registration is cleared 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

func NewErrResult(err error) *Result

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

func (r *Result) Close() error

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) Columns

func (r *Result) Columns() []string

Columns returns the ordered list of output column names.

func (*Result) Counters added in v0.11.0

func (r *Result) Counters() *exec.QueryCounters

Counters returns the write effects this statement actually applied — nodes and relationships created and deleted, properties set and removed, labels added and removed, and index and constraint DDL (#2212).

It returns nil for a read-only statement. That is a meaningful distinction, not an absence: nil means the statement had no write surface at all, whereas a non-nil all-zero result means writes were attempted and changed nothing — the difference between a MATCH and a MERGE that matched.

The counts reflect what was APPLIED, so a re-intern of an existing node is not a creation, removing an absent property counts nothing, and a statement that failed or rolled back never produces a Result to report from. The nodes and relationships counters are incremented at the same call sites as the graph-scoped side-effect counters the openCypher TCK comparator verifies, so the two cannot drift.

The returned pointer is owned by the Result; treat it as read-only.

func (*Result) Err

func (r *Result) Err() error

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) IsClosed

func (r *Result) IsClosed() bool

IsClosed reports whether Close has been called on this Result.

func (*Result) Next

func (r *Result) Next() bool

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

func (r *Result) Record() exec.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

func (r *Result) RowAt(i int) []expr.Value

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

func (r *Result) ValueAt(col int) expr.Value

ValueAt returns the value at column index col of the current row. It must only be called after a successful [Next], 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. Out-of-range indices return nil.

It serves BOTH a materialised result and a streaming one. That distinction used to matter and silently produced wrong answers (#2215): SHOW INDEXES and SHOW CONSTRAINTS build a streaming result over static rows ([Engine.newShowResult]), while this accessor read only the materialised backing store — whose matRowLen is zero for a streaming result, so every column came back as a bare nil. bolt/server reads each column with ValueAt on the premise that an engine result is always materialised, so `SHOW INDEXES` over Bolt answered with a well-formed row of nulls and no error: fail-silent, which the failure-handling rule forbids outright. Record() was unaffected, which is why the in-tree tests missed it.

The streaming branch is served from the ResultSet's positional row, so it costs no allocation and no map lookup, and it fixes the whole class rather than the one instance — any future non-materialised plan is now safe here.

type Session added in v0.11.0

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

Session is a Cypher caller that observes its own committed writes.

Every statement run through it still gets snapshot isolation. What it adds is the cross-statement guarantee the bare Engine does not make: a read issued after this session's own commit observes that commit.

Obtain one with Engine.NewSession. A Session holds no resources and needs no close; it is a floor timestamp and a pointer to the engine.

func (*Session) BeginReadTx added in v0.11.0

func (s *Session) BeginReadTx(ctx context.Context) (*ExplicitTx, error)

BeginReadTx opens a read-only transaction bound to this session, pinned to an instant at or after this session's last commit.

func (*Session) BeginTx added in v0.11.0

func (s *Session) BeginTx(ctx context.Context) (*ExplicitTx, error)

BeginTx opens a multi-statement write transaction bound to this session: it observes every commit the session has made, and its own commit instant is recorded on the session when it closes.

The recording happens on Commit AND on Rollback, for the reason lpg.Session documents: a rolled-back statement still publishes at the lpg layer, so its instant is a real published instant and a session that skipped it could start its next operation below a commit it made.

func (*Session) Floor added in v0.11.0

func (s *Session) Floor() uint64

Floor reports the instant of the latest commit this session has made, or 0 when it has made none. It is the timestamp every subsequent operation waits for.

Exported for observability and for tests that need to bracket an observation between two known instants; ordinary callers never need it.

func (*Session) Run added in v0.11.0

func (s *Session) Run(ctx context.Context, query string, params map[string]expr.Value) (*Result, error)

Run executes a read-only statement, observing every commit this session has made.

It waits for the visible frontier to reach this session's floor and then runs exactly as Engine.Run does. On a session that has committed nothing the wait is a single atomic load and the statement is indistinguishable from Engine.Run.

func (*Session) RunAny added in v0.11.0

func (s *Session) RunAny(ctx context.Context, query string, params map[string]any) (*Result, error)

RunAny is Engine.RunAny bound to this session: it binds `any`-typed parameters and routes to Session.RunInTx or Session.Run by whether the query writes.

It is the autocommit entry point a server uses, because a wire protocol hands it untyped parameters and does not tell it whether the statement writes.

func (*Session) RunInTx added in v0.11.0

func (s *Session) RunInTx(ctx context.Context, query string, params map[string]expr.Value) (*Result, error)

RunInTx executes a writing statement as one autocommit transaction, observing every commit this session has made and recording its own.

Recording its own is the half that makes the NEXT operation correct: without it a session's guarantee would cover only the writes it made before the last one.

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.

Jump to

Keyboard shortcuts

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