cypher

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 5, 2026 License: MIT Imports: 30 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 serialise on a single-writer lock: the backing txn.Store's writer mutex when the engine is WAL-backed (NewEngineWithStore), or the engine's own writer mutex when it is store-less (NewEngine). Autocommit Engine.RunInTx holds that lock for one statement; an explicit transaction (Engine.BeginTx) holds it from BEGIN until COMMIT/ROLLBACK, so concurrent writers block until it finishes (write-write isolation). Reads (Engine.Run) never take the writer lock. The lock order is writer-lock (outermost) → the graph visibility barrier (visMu, inside lpg.Graph.ApplyAtomically); both wirings share it, so no deadlock is possible across them.

Transactions

Engine.RunInTx is autocommit: each call is its own all-or-nothing, durable-then-visible transaction. Engine.BeginTx opens an explicit, multi-statement transaction (ExplicitTx) whose statements commit or roll back together — the engine substrate for the Bolt BEGIN/RUN/COMMIT/ROLLBACK protocol. Both apply writes eagerly to the in-memory graph and roll back via the in-memory undo log on error; a concurrent reader can therefore observe an open transaction's not-yet-committed writes (read-uncommitted for readers). See ExplicitTx (exectx.go) for the full transaction and isolation contract.

Index

Examples

Constants

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

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 for unsupported types.

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.

Types

type ConstraintDef added in v0.2.0

type ConstraintDef struct {
	// Unique is true for a UNIQUE constraint, false for a NOT NULL constraint.
	Unique bool
	// 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
}

ConstraintDef is a durable constraint definition handed to the engine on open so it can re-register a constraint recovered from disk. It mirrors store/recovery.ConstraintRecord without coupling callers to the recovery package's wire types; ConstraintDefsFromRecovery converts a recovery result into this form.

func ConstraintDefsFromRecovery added in v0.2.0

func ConstraintDefsFromRecovery(recovered []recovery.ConstraintRecord) []ConstraintDef

ConstraintDefsFromRecovery converts the durable constraint set surfaced by store/recovery.Open into the ConstraintDef slice the engine constructor accepts via [EngineOptions.RecoveredConstraints]. Pass it the store/recovery.Result.Constraints field. The recovery package's wire kind (txn.ConstraintKind: 0 = UNIQUE, 1 = NOT NULL) is mapped to the boolean [ConstraintDef.Unique].

type Engine

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

Engine is the public query engine. It binds a graph, a function registry, and a plan cache, and exposes a single Run method for query execution.

Engine is safe for concurrent use. A single Engine may serve any number of concurrent Engine.Run readers together with concurrent Engine.RunInTx writers: each call builds its own operator tree, the plan cache is internally synchronised, and both the physical-plan build and execution run under the graph's visibility barrier (lpg.Graph.View for reads, lpg.Graph.ApplyAtomically for writes). A writer that grows the node space can therefore never tear a concurrent reader's plan build, and readers never observe a partially-applied write transaction (#1077, audit gap F3).

Write queries remain subject to the underlying store's single-writer constraint: when the Engine is backed by a txn.Store, concurrent Engine.RunInTx calls serialise on the store's writer mutex.

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.

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 (*Engine) BeginTx added in v0.2.0

func (e *Engine) BeginTx(ctx context.Context) (*ExplicitTx, error)

BeginTx opens an explicit, multi-statement transaction bound to ctx and acquires the engine's writer serialisation: the store's single-writer mutex on a WAL-backed engine, or the engine writer mutex on a store-less engine. The caller MUST finish the returned handle with exactly one ExplicitTx.Commit or ExplicitTx.Rollback; until then the writer serialisation is held and concurrent writers block (write-write Isolation).

ctx bounds every statement executed through the handle. Pass the connection context (optionally narrowed with a transaction timeout) so that a cancelled connection, a server shutdown, or an elapsed timeout interrupts an in-flight statement and guarantees the writer serialisation cannot be held forever.

If ctx is already cancelled or its deadline has elapsed, BeginTx returns promptly without acquiring any lock, with an error wrapping the context error (matchable via errors.Is against context.Canceled / context.DeadlineExceeded).

See exectx.go for the full transaction and concurrency contract, including the documented read-uncommitted-for-readers isolation scope.

func (*Engine) ClearPlanCache

func (e *Engine) ClearPlanCache()

ClearPlanCache drops every cached plan and increments the cypher.plan_cache.invalidations counter exactly once. It is the operator-facing invalidation hook installed on every DDL operator (CREATE/DROP INDEX, CREATE/DROP CONSTRAINT) — successful schema mutations call it so that subsequent queries re-plan against the new index / constraint topology rather than reusing stale plans built before the schema changed.

ClearPlanCache is also safe to invoke directly as a user-facing manual reset (e.g. from operational tooling after an out-of-band index swap on the underlying graph).

ClearPlanCache is idempotent and safe for concurrent use; each call emits exactly one invalidations counter increment regardless of the cache's prior size.

func (*Engine) ConstraintSpecsForSnapshot added in v0.2.0

func (e *Engine) ConstraintSpecsForSnapshot() []snapshot.ConstraintSpec

ConstraintSpecsForSnapshot converts the engine's current constraint set into the store/snapshot.ConstraintSpec slice that store/snapshot.WriteSnapshotFullWithConstraints (and its mapper-codec variant) persists into a snapshot's constraints.bin component. A checkpointer calls e.ConstraintSpecsForSnapshot() and hands the result to the writer so a checkpoint + WAL truncate does not lose constraints.

func (*Engine) Constraints added in v0.2.0

func (e *Engine) Constraints() []ConstraintDef

Constraints returns a structured snapshot of every schema constraint currently registered on the engine, in deterministic order (UNIQUE before NOT NULL, then by label, property, name). It is the source a checkpointer passes to store/snapshot.WriteSnapshotFullWithConstraints (via [ConstraintSpecsForSnapshot]) so the constraint set survives a checkpoint that truncates the WAL prefix which first declared a constraint (audit gap H1, the checkpoint-survival half).

Constraints is safe for concurrent use.

func (*Engine) Explain

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

Explain returns a textual representation of the physical plan that would be chosen to execute query with the given params. The plan reflects current index availability: a hash index on the relevant (label, property) pair causes the relevant Selection+LabelScan subtree to appear as NodeByIndexSeek. No rows are produced; the graph is not modified.

The format mirrors ir.Explain but annotates Selection→LabelScan pairs that would be rewritten to index seeks at execution time.

Example

ExampleEngine_Explain returns the logical plan for a query as text without executing it or touching the graph.

package main

import (
	"fmt"

	"github.com/FlavioCFOliveira/GoGraph/cypher"
	"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
	"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)

func main() {
	g := lpg.New[string, float64](adjlist.Config{})
	eng := cypher.NewEngine(g)

	plan, err := eng.Explain("MATCH (n:Person) RETURN n", nil)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Print(plan)
}
Output:
ProduceResults
└─ Projection
   └─ NodeByLabelScan

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

Run parses, analyses, plans, and executes query, returning a materialised Result. The query is built and drained inside the read visibility barrier (Graph.View) so it observes a consistent, partial-transaction-free snapshot. DDL statements take a dedicated fast path. Parameters are bound from params and type-checked against the plan before execution.

If ctx is already cancelled or its deadline has elapsed when Run is called, it returns promptly — before any parse, plan, or execution work — with an error wrapping the context error (matchable via errors.Is against context.Canceled / context.DeadlineExceeded).

A recoverable panic raised while planning or executing the query is intercepted and returned as an error wrapping ErrInternalPanic; it never unwinds past this method to crash the embedding process.

Example

ExampleEngine_Run runs a read query against a populated graph and reads a scalar aggregate from the streaming result. Result must always be closed.

package main

import (
	"context"
	"fmt"

	"github.com/FlavioCFOliveira/GoGraph/cypher"
	"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
	"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)

func main() {
	g := lpg.New[string, float64](adjlist.Config{})
	for _, key := range []string{"a", "b", "c"} {
		if err := g.AddNode(key); err != nil {
			fmt.Println("error:", err)
			return
		}
		if err := g.SetNodeLabel(key, "Person"); err != nil {
			fmt.Println("error:", err)
			return
		}
	}

	eng := cypher.NewEngine(g)
	res, err := eng.Run(context.Background(), "MATCH (n:Person) RETURN count(n) AS people", nil)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer res.Close()

	for res.Next() {
		rec := res.Record()
		fmt.Println("people:", rec["people"])
	}
}
Output:
people: 3

func (*Engine) RunAny

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.

For the current in-memory implementation there is no external transaction manager (lpg.Graph does not support rollback). "Commit on success, rollback on error" means: the pipeline runs to completion with mutations applied eagerly; if any operator returns an error the pipeline is drained no further (standard Volcano error propagation) and the partial mutations remain in the graph. This matches the single-writer, in-memory contract documented in CLAUDE.md.

RunInTx is safe for concurrent use (each call creates an independent operator tree), subject to the single-writer constraint on write queries.

If ctx is already cancelled or its deadline has elapsed when RunInTx is called, it returns promptly — before any parse, plan, or txn.Store.Begin work — with an error wrapping the context error (matchable via errors.Is against context.Canceled / context.DeadlineExceeded).

Example

ExampleEngine_RunInTx executes a CREATE inside a transaction (atomic and, for WAL-backed engines, durable) and then reads the data back in a second query.

package main

import (
	"context"
	"fmt"

	"github.com/FlavioCFOliveira/GoGraph/cypher"
	"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
	"github.com/FlavioCFOliveira/GoGraph/graph/lpg"
)

func main() {
	g := lpg.New[string, float64](adjlist.Config{})
	eng := cypher.NewEngine(g)

	// Write: CREATE two labelled nodes atomically.
	write, err := eng.RunInTx(context.Background(),
		`CREATE (:Account {owner: "alice"}), (:Account {owner: "bob"})`, nil)
	if err != nil {
		fmt.Println("write error:", err)
		return
	}
	for write.Next() { //nolint:revive // a write query streams no result rows; drain then close
	}
	if err := write.Err(); err != nil {
		fmt.Println("write error:", err)
		return
	}
	write.Close()

	// Read-back: the committed nodes are visible to a subsequent query.
	read, err := eng.Run(context.Background(), "MATCH (a:Account) RETURN count(a) AS accounts", nil)
	if err != nil {
		fmt.Println("read error:", err)
		return
	}
	defer read.Close()
	for read.Next() {
		fmt.Println("accounts:", read.Record()["accounts"])
	}
}
Output:
accounts: 2

func (*Engine) RunInTxAny

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

RunInTxAny executes a write query with params expressed as map[string]any, automatically converting Go native types to expr.Value. See BindParams.

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]

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

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

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

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

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.

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 the engine's writer serialisation for its whole lifetime (write-write Isolation); it is NOT safe for concurrent use by multiple goroutines.

func (*ExplicitTx) Commit added in v0.2.0

func (tx *ExplicitTx) Commit() (err error)

Commit makes the whole transaction durable and visible, then releases the writer serialisation. On a WAL-backed engine the WAL is fsynced exactly ONCE for every statement's accumulated writes (durable-then-visible, #1281) and the secondary-index buffer is committed; on a store-less engine the writes are already visible and Commit simply finalises the index buffer. The accumulated undo log is discarded. After Commit the handle is finished.

Commit runs the finalisation inside the visibility barrier so that, on a WAL-backed engine, the fsync happens-before the index commit and no concurrent reader can observe a committed-but-not-durable state. If the WAL fsync fails, the transaction is rolled back instead (in-memory undo replayed, index and WAL rolled back) and the fsync error is returned wrapping it: a transaction whose durability could not be guaranteed is reported as failed, never acknowledged.

Commit returns ErrTxFinished if the transaction was already committed or rolled back.

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 returns that error AND leaves the transaction open: the per-statement writes remain in the accumulated undo log, so the caller (the Bolt session) decides whether to roll the whole transaction back. 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 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 single-writer mutex is released at that point — a leaked, unclosed Result leaks only the ResultSet, not the write lock. Callers that need predictable resource release MUST still call Close themselves.

Result is NOT safe for concurrent use.

func (*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) 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.

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

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.
ir
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.
rewrite
Package rewrite provides a rule-based logical-plan rewrite/optimisation framework for the Cypher IR.
Package rewrite provides a rule-based logical-plan rewrite/optimisation framework for the Cypher IR.
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 plan provides the cost-based planner for the Cypher executor.
Package plan provides the cost-based planner for the Cypher executor.
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