Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

MarsDB is an embeddable property-graph database with an openCypher query subset: single binary, single file, optional in-memory mode. No server process, no network protocol — it links into your Rust, Python, or Go program (or runs standalone via the marsdb CLI) the same way SQLite does for relational data.

$ marsdb :memory:
MarsDB graph database. Enter Cypher statements terminated by `;`. Ctrl-D to exit.
marsdb> CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'});
marsdb> MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name;
a.name | b.name
Alice | Bob

Why MarsDB

  • openCypher subset, measured against the real spec. MarsDB is checked against the openCypher Technology Compatibility Kit (TCK) — 3,880 real conformance scenarios — not just its own test suite. See Cypher Language Support for the exact, current pass rate and what’s covered.
  • Embeddable, not a server. Database::open("path/to.db") or Database::in_memory() and you’re running queries — no daemon to manage, no port to bind, no client/server protocol.
  • Crash-safe by construction. Every Cypher statement runs inside one transaction; marsdb-storage runs on redb, a pure-Rust MVCC single-file engine, and a SIGKILL-and-reopen crash harness checks that every acknowledged commit survives intact.
  • Bindings, not just a Rust crate. Python (PyO3, prebuilt wheels) and Go (cgo against a small C ABI crate) both work today — see Python bindings and Go bindings.

Where to go next

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

Install & CLI

Install

CLI — installs the marsdb binary:

cargo install marsdb-cli

Or on macOS/Linux via Homebrew (tap):

brew install knoguchi/marsdb/marsdb

Usage

marsdb                                  # in-memory REPL
marsdb mydata.db                        # file-backed REPL
marsdb mydata.db "MATCH (n) RETURN n"   # run one query, exit
marsdb :memory: "..."                   # explicit in-memory, one-shot
marsdb mydata.db "CREATE (a); CREATE (b); MATCH (n) RETURN n"  # ;-separated batch
marsdb mydata.db < script.cypher        # piped stdin, same ;-separated batch
marsdb mydata.db --nl "who does Alice know?"  # plain-English question via Ollama

The REPL accepts any Cypher statement terminated by ;. Ctrl-D exits.

Natural language queries

--nl translates a plain-English question into Cypher and runs it, using a local Ollama instance:

ollama serve &
ollama pull llama3.2
marsdb mydata.db --nl "how many people are there?"

Set OLLAMA_MODEL to use a model other than the default llama3.2. The generated Cypher is printed before its results. Generated writes are rejected — --nl only ever runs read-only queries. The translation itself validates the generated Cypher’s syntax and variable/type binding against the database’s actual schema, retrying once with the validation error fed back if the first attempt doesn’t parse — see the marsdb-nl2cypher crate for the underlying library.

$ marsdb :memory:
MarsDB graph database. Enter Cypher statements terminated by `;`. Ctrl-D to exit.
marsdb> CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'});
marsdb> MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name;
a.name | b.name
Alice | Bob

Embedding in Rust

cargo add marsdb
#![allow(unused)]
fn main() {
let db = marsdb::Database::in_memory()?; // or Database::open("path/to.db")
db.execute("CREATE (a:Person {name: 'Alice'})")?;
let result = db.execute("MATCH (n:Person) RETURN n.name")?;

// Bound work from untrusted callers. A CancellationToken can also be
// cloned and cancelled from another thread.
let options = marsdb::ExecutionOptions {
    max_intermediate_rows: Some(100_000),
    max_result_rows: Some(10_000),
    max_relationship_expansions: Some(1_000_000),
    timeout: Some(std::time::Duration::from_secs(5)),
    ..Default::default()
};
let result = db.execute_with_options("MATCH (n) RETURN n", &options)?;

// Group statements into one atomic unit. Reads through `tx` see its earlier
// writes; any statement error aborts and closes the whole transaction.
let mut tx = db.begin_transaction()?;
tx.execute("CREATE (:Person {name: 'Bob'})")?;
tx.execute("CREATE (:Person {name: 'Carol'})")?;
tx.commit()?;

// Operational checks and crash-consistent backup. The backup destination
// must be new, so an existing file is never overwritten.
db.backup_to("path/to-backup.db")?;
let mut db = db;
let report = db.check_integrity()?;
assert_eq!(report.nodes, 3);

// Or run a `;`-separated batch, one transaction per statement, one
// QueryResult per statement back:
let results = db.execute_batch("CREATE (a:Person {name: 'Alice'}); CREATE (b:Person {name: 'Bob'})")?;
}

ExecutionOptions::observer accepts an ExecutionObserver callback for dependency-free telemetry. Events contain duration, outcome category, read/write classification, result-row count, and relationship expansions; they deliberately exclude query text and error messages. Syntax and missing-parameter rejections are reported too, and observer panics are contained.

Stored procedures (CALL)

MarsDB ships no built-in procedures — CALL proc(args) [YIELD ...] resolves against a marsdb_query::ProcedureProvider you supply via ExecutionOptions::procedures:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use marsdb::{ExecutionOptions, Procedures, ProcedureProvider, ProcedureSignature, Value};
// `ProcedureProvider::call`'s error type -- not re-exported by `marsdb`
// itself, so implementing the trait needs a direct `marsdb-query`
// dependency too (`cargo add marsdb-query`).
use marsdb_query::QueryError;

struct MyProcedures;

impl ProcedureProvider for MyProcedures {
    fn signature(&self, name: &str) -> Option<ProcedureSignature> {
        // Look up `name`'s declared inputs/outputs, or `None` if unknown.
        None
    }
    fn call(&self, name: &str, args: &[Value]) -> Result<Vec<Vec<Value>>, QueryError> {
        // Run the procedure, return its output rows.
        Ok(vec![])
    }
}

let options = ExecutionOptions {
    procedures: Some(Procedures(Arc::new(MyProcedures))),
    ..Default::default()
};
}

More examples

cargo run -p marsdb --example task_tracker    # CRUD + aggregation
cargo run -p marsdb --example social_graph    # variable-length traversal, MATCH...CREATE
cargo run -p marsdb --example params_and_batch # $parameters, execute_batch

Full source in marsdb/examples/. Each also writes an SVG chart of its query result (via plotters) to the current directory.

Natural language → Cypher

marsdb-nl2cypher translates an English question into Cypher against a database’s actual schema (labels/relationship-types/properties in use, introspected automatically), validates its syntax and variable/type binding, and retries once with the exact validation error fed back if the first attempt is invalid. No HTTP/LLM-SDK dependency in the core crate — bring your own LlmClient:

#![allow(unused)]
fn main() {
use marsdb::Database;
use marsdb_nl2cypher::{translate_and_run, LlmClient};

let db = Database::in_memory()?;
db.execute("CREATE (:Person {name: 'Alice'})-[:KNOWS]->(:Person {name: 'Bob'})")?;

let (cypher, result) = translate_and_run(&db, &my_llm_client, "who does Alice know?")?;
}

translate_and_run enforces read-only generated Cypher. Model-generated writes are rejected before execution unless the caller explicitly uses translate_and_run_with_policy(..., ExecutionPolicy::AllowWrites) after performing its own authentication and authorization.

MarsDB’s narrower Cypher subset (vs. full Neo4j Cypher) is a deliberate fit for this — a smaller grammar means fewer ways an LLM can generate something unparseable. The prompt tells the model what’s supported and what to avoid (no bare --> shorthand, MERGE capped at one hop, etc.) — see marsdb-nl2cypher/src/lib.rs’s CAPABILITIES constant.

A real, runnable example against a local Ollama instance:

ollama serve &
ollama pull llama3.2
cargo run -p marsdb-nl2cypher --example ollama_demo

C API

marsdb-capi is a small, hand-written C ABI (opaque handle + JSON results) — the basis for non-Rust bindings like Go. Header: marsdb-capi/marsdb.h.

typedef struct MarsdbDatabase MarsdbDatabase;

/* Exactly one of `json`/`error` is non-null. Both, when non-null, must be
 * released with marsdb_free_string -- never with free(). */
typedef struct MarsdbResult {
    char *json;
    char *error;
} MarsdbResult;

/* Open (creating if absent) a single-file, on-disk database. Returns NULL
 * on failure (bad UTF-8 path, or the underlying open erroring). */
MarsdbDatabase *marsdb_open(const char *path);

/* Open a purely in-memory database. Nothing is written to disk. */
MarsdbDatabase *marsdb_open_in_memory(void);

/* Reclaims a handle from marsdb_open/marsdb_open_in_memory. NULL is a
 * no-op; double-close or closing a foreign pointer is undefined behavior. */
void marsdb_close(MarsdbDatabase *db);

/* Run one Cypher statement. Result JSON shape:
 *   {"columns": [...], "rows": [[...], ...]}
 * where each value is its natural JSON scalar, or for nodes/edges:
 *   {"__type": "node", "id": ..., "labels": [...], "props": {...}}
 *   {"__type": "edge", "id": ..., "label": ..., "src": ..., "dst": ..., "props": {...}}
 * Dates and durations use canonical ISO-8601 strings.
 */
MarsdbResult marsdb_execute(MarsdbDatabase *db, const char *cypher);

/* Frees a string returned in MarsdbResult.json or MarsdbResult.error.
 * Required -- these are allocated by Rust's global allocator, not malloc. */
void marsdb_free_string(char *s);

Build the shared/static library with cargo build -p marsdb-capi (add --release for an optimized build) — produces libmarsdb_capi.{dylib,so,a} under target/{debug,release}/.

This is intentionally minimal (Open/Execute/Close, JSON results) — enough to build a real language binding on top, which is exactly what marsdb-go does. execute_batch/execute_with_params exist on the Rust side but aren’t exposed through this C ABI yet.

Python bindings

pip install marsdb
import marsdb
db = marsdb.Database.in_memory()  # or .open(path)
db.execute("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})")
db.execute("MATCH (n:Person) RETURN n.name")
# -> [{'n.name': 'Alice'}, {'n.name': 'Bob'}]

Prebuilt wheels cover macOS (arm64, x86_64) and Linux (x86_64, manylinux); other platforms install from the source distribution and need a Rust toolchain. Built via PyO3 — in-process, no separate server or IPC.

Build from source

cd marsdb-python
python3 -m venv .venv && source .venv/bin/activate
pip install maturin && maturin develop

Go bindings

Unlike marsdb-python (PyO3, in-process), Go has no equivalent in-process FFI story with Rust, so marsdb-go goes through the small C ABI crate, marsdb-capi, via cgo.

go get github.com/knoguchi/marsdb/marsdb-go

Go modules resolve straight from the public Git host, no separate registry step — this works today even though the module isn’t semver-tagged yet (resolves to a pseudo-version off main). The C ABI side still needs building locally either way (cgo can’t fetch a prebuilt .dylib/.so), so most users will clone the repo and build both pieces as below.

Build

Two steps: build the Rust cdylib, then build the Go package against it.

# 1. Build marsdb-capi (produces target/debug/libmarsdb_capi.dylib on macOS)
cargo build -p marsdb-capi

# 2. Build/test the Go package
cd marsdb-go
go build ./...
go test ./...

marsdb.go’s cgo preamble already points -L/-I at ../target/debug/../marsdb-capi relative to this directory via cgo’s ${SRCDIR} substitution, so the two commands above work as-is right after a debug build on macOS. On Linux, add the shared-library directory at runtime:

LD_LIBRARY_PATH="$(pwd)/../target/debug" go test ./...

If you built marsdb-capi in release mode instead (cargo build -p marsdb-capi --release), override the link path:

CGO_LDFLAGS="-L$(pwd)/../target/release -lmarsdb_capi" go build ./...

Usage

package main

import (
	"fmt"
	"log"

	marsdb "github.com/knoguchi/marsdb/marsdb-go"
)

func main() {
	db, err := marsdb.InMemory() // or marsdb.Open("path/to.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	if _, err := db.Execute("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})"); err != nil {
		log.Fatal(err)
	}

	rows, err := db.Execute("MATCH (n:Person) RETURN n.name AS name ORDER BY n.name")
	if err != nil {
		log.Fatal(err)
	}
	for _, row := range rows {
		fmt.Println(row["name"])
	}
	// Alice
	// Bob
}

A runnable copy lives in examples/basic:

cargo build -p marsdb-capi
cd marsdb-go && go run ./examples/basic

Execute returns []map[string]any, one map per matched row keyed by column name — the same dict-per-row shape as marsdb-python. A returned node decodes as map[string]any{"__type": "node", "id": ..., "labels": []any{...}, "props": map[string]any{...}}; an edge similarly with "__type": "edge" plus "src"/"dst". Integer properties and IDs retain their full precision as int64 (or uint64 for an ID above int64’s range), while fractional values are float64. Dates and durations are returned as canonical ISO-8601 strings such as "1984-10-11" and "P1M2D".

What’s not here yet

Only Open/InMemory/Execute/Closeexecute_batch (multi-statement, one transaction each) and execute_with_params ($param substitution) exist on the Rust/C ABI side’s natural extension points but aren’t wired through marsdb-capi or this package yet. Not yet set up to produce a redistributable Go binary to a machine without this exact local build layout either — see the package README for the exact gap (static-linking libmarsdb_capi.a, or @rpath-relative dylib linking).

Cypher Language Support

MarsDB implements a real subset of openCypher, checked against the openCypher Technology Compatibility Kit (TCK) — 220 feature files, 3,880 scenarios, vendored as a git submodule and run for real (not just claimed) on every push. The full, exhaustive breakdown — every supported clause/expression/temporal-type shape, the error taxonomy, and this same table with contributor-level detail — lives in CYPHER_COVERAGE.md in the repo root. This page is the condensed, end-user version.

Conformance, by category

categorytotalpasspass %
clauses/call5252100.0%
clauses/create7878100.0%
clauses/delete4141100.0%
clauses/match381381100.0%
clauses/match-where3434100.0%
clauses/merge7575100.0%
clauses/remove3333100.0%
clauses/return6363100.0%
clauses/return-orderby3535100.0%
clauses/return-skip-limit3131100.0%
clauses/set5353100.0%
clauses/union1212100.0%
clauses/unwind1414100.0%
clauses/with2929100.0%
clauses/with-orderBy292292100.0%
clauses/with-skip-limit99100.0%
clauses/with-where1919100.0%
expressions/aggregation3535100.0%
expressions/boolean150150100.0%
expressions/comparison7272100.0%
expressions/conditional1313100.0%
expressions/existentialSubqueries1010100.0%
expressions/graph6161100.0%
expressions/list185185100.0%
expressions/literals131131100.0%
expressions/map4444100.0%
expressions/mathematical66100.0%
expressions/null4444100.0%
expressions/path77100.0%
expressions/pattern5050100.0%
expressions/precedence104104100.0%
expressions/quantifier604604100.0%
expressions/string3232100.0%
expressions/temporal*1004100299.8%
expressions/typeConversion4747100.0%
useCases/countingSubgraphMatches1111100.0%
useCases/triadicSelection1919100.0%
TOTAL3880387899.9%

* The 2 non-passing expressions/temporal scenarios need dates at year ±999,999,999 — a real, structural storage/library-range limitation (not a bug), explained in full in CYPHER_COVERAGE.md.

Reproduce this table yourself:

git submodule update --init marsdb-tck/openCypher
cargo run --release -p marsdb-tck

What’s supported

  • Patterns: MATCH/OPTIONAL MATCH, undirected/bracketless/multi-type/ variable-length relationship patterns, named-path capture (including over variable-length hops), shortestPath(), pattern comprehension, pattern predicates.
  • Reading & filtering: WHERE (property/identity/label comparisons, pattern predicates, STARTS WITH/ENDS WITH/CONTAINS), exists { ... } (both the simple pattern form and the full nested-subquery form, including nested exists {}), UNWIND, any number of chained WITH boundaries, ORDER BY/SKIP/LIMIT, DISTINCT.
  • Writing: CREATE, MERGE (with ON CREATE/ON MATCH), SET, REMOVE, DELETE/DETACH DELETE, MATCH ... CREATE, arbitrary chaining of mutating clauses via trailing WITH or RETURN. MERGE is capped at one relationship hop.
  • Aggregation: implicit GROUP BY, count/sum/avg/min/max/ collect/percentileCont/percentileDisc, DISTINCT inside an aggregate call, aggregate expressions composed with arithmetic.
  • Functions: the standard scalar/string/math/list function set (coalesce, toInteger/toFloat/toString/toBoolean, keys/ labels/type/properties/id, size/length/nodes/ relationships/head/tail/last/range, toUpper/toLower/ trim/replace/split/substring, abs/ceil/floor/round/ sqrt/sign).
  • Temporal types: Date/LocalTime/Time/LocalDateTime/ DateTime/Duration — construction from strings, maps, or another temporal value; comparison; calendar-aware arithmetic; full component access; ISO-8601 string round-tripping.
  • Stored procedures: CALL proc(args) [YIELD ...], both standalone and in-query forms, against a caller-supplied ProcedureProvider — see Embedding in Rust. MarsDB itself ships no built-in procedures.
  • Parameters: $name scalars, lists (including nested lists), and maps.

Known limitations

  • Extreme-year temporal values (year beyond roughly ±262,000): a real storage/library range limit, not planned to change without a breaking on-disk format migration — see the table footnote above.
  • Node/relationship-valued $parameters aren’t supported (a map-valued parameter is).
  • No cost-based query optimizer — index seeks and top-k ORDER BY ... LIMIT selection exist, but join/traversal ordering isn’t cost-estimated. See Architecture.
  • CALL needs an embedder-supplied ProcedureProvider — there’s no built-in procedure catalog.

Architecture

marsdb-storage   thin trait boundary over redb (file + in-memory backends)
marsdb-graph     property graph model, CRUD, KV/adjacency encoding
marsdb-query     openCypher subset: ANTLR4 grammar -> AST -> IR -> executor
marsdb           embeddable public Rust API (Database::open/in_memory/execute)
marsdb-cli       the `marsdb` binary (REPL + one-shot mode)
marsdb-python    PyO3 bindings, builds via maturin
marsdb-capi      C ABI (opaque handle + JSON results), basis for non-Rust bindings
marsdb-go        Go bindings, via cgo against marsdb-capi
marsdb-nl2cypher natural-language -> Cypher: schema introspection, prompt building, validate-and-repair

Storage

Storage runs on redb, a pure-Rust single-file MVCC embedded KV engine. Every Cypher statement runs inside one transaction — a read-only MATCH ... RETURN opens a ReadTransaction (a consistent snapshot that runs alongside other concurrent readers or a concurrent writer without contending for redb’s single-writer lock), everything else opens a WriteTransaction, committed or aborted as a whole. Database::begin_transaction lets callers explicitly extend that atomic boundary across multiple statements. MarsDB records its own table/record format version in metadata when the file is created or first opened by a version-aware build, and refuses to open a database written by a newer unsupported format.

A from-scratch storage engine (page format, B-tree, crash recovery) as an alternate marsdb-storage backend independent of redb is on the roadmap, not built yet — the trait boundary in marsdb-storage exists specifically so a second backend could slot in later without touching marsdb-graph/ marsdb-query.

Query execution

Query execution compiles Cypher to a small Gremlin-shaped logical IR (AllNodesScan, NodeByLabelScan, Seed, Expand, VarExpand, Filter, IndexSeek) so a future Gremlin frontend could target the same executor. The parser is ANTLR4-generated (marsdb-query/grammar/), replacing an earlier hand-rolled pest grammar.

The logical read plan runs as a pull-based row stream through node-ID scans, filters, and relationship expansions. A non-aggregating, non-distinct RETURN ... LIMIT k without ORDER BY stops that pipeline after k rows, so downstream limits avoid unnecessary expansions. Clause boundaries and inherently blocking operations still materialize: WITH, optional-match reconciliation, variable-length traversal results for each input row, aggregation, DISTINCT, mutations, and the public QueryResult. Use ExecutionOptions to put hard ceilings on intermediate rows, result rows, relationship expansions, and elapsed time.

There is not yet a general cost-based optimizer. Two targeted optimizations complement streaming: a direct MATCH (n[:Label]) RETURN ... LIMIT k scan pushes the limit into storage; and every ORDER BY ... LIMIT k site uses a top-k partial selection (slice::select_nth_unstable_by + a sort of just the k-sized prefix) instead of a full sort of every row. Declared property indexes (CREATE INDEX ON :Label(prop)) are used automatically when a WHERE/ inline-property equality matches one — see the index seek benchmarks.

Cypher coverage

See Cypher Language Support for the full, TCK-measured breakdown.

Benchmarks

Measured on a single MacBook (Apple Silicon, arm64), release build, in-process (cargo bench, criterion). No other graph database was benchmarked under the same conditions — these numbers aren’t a competitive comparison, they’re here to track regressions and show where the current architecture’s cost is. Full detail, every dataset size, and the story behind each number: BENCHMARKS.md in the repo root.

Reproduce: cargo bench -p marsdb-graph and cargo bench -p marsdb (runs cypher_ops, ldbc_ops, aggregate_ops, concurrency_ops, and index_ops).

Storage layer

OperationResult
create_node37.1 µs
create_edge50.6 µs
get_node (point lookup by id)832 ns
neighbors, 1-hop, fanout 101.05 µs
neighbors, 1-hop, fanout 1,00054.9 µs
all_nodes scan, label matches 1% of rows, 100,000 rows801 µs

NODE_LABEL_INDEX backs label-filtered scans — roughly 30-80x faster than a full scan when only a small fraction of rows match, staying close to flat per matching row as the table grows.

Cypher layer

OperationResult
Parse + execute, 10-hop CREATE3.58 ms
MATCH (n)-[:R]->(m) RETURN m.idx LIMIT 10, 10,000-node dataset1.284 ms
MATCH (n:Label) RETURN n LIMIT 10 (no hop/WHERE/ORDER BY), 100,000-node dataset22.4 µs

The last row is the direct payoff of pushing LIMIT into the storage scan: flat ~19-22 µs regardless of dataset size (a ~1,000x-larger table costs barely 17% more) — it really does stop at the first LIMIT matches instead of scanning the whole table first.

Property indexes

MATCH (n:Item {idx: N}) RETURN n.idx with and without CREATE INDEX ON :Item(idx) declared:

Dataset sizeUnindexed scanIndex seekSpeedup
10078.6 µs7.36 µs10.7x
10,0008.43 ms7.87 µs1,071x
100,00092.4 ms7.72 µs~12,000x

The index seek stays flat regardless of dataset size — it reads exactly the matching entries, never touches the rest of the table.

Aggregation

resolve_grouped_rows (the grouping core behind count/sum/avg/ min/max/collect and implicit GROUP BY) uses a hash-based group lookup rather than a linear scan:

OperationResult (10,000 rows)
Global aggregate (1 group)54.5 ms
GROUP BY cat (10 groups)30.9 ms
GROUP BY cat (every row its own group)35.3 ms
collect(n.idx)20.1 ms

All of these scale close to linearly with row count.

Concurrent reads

A MATCH ... RETURN opens a ReadTransaction, not a WriteTransaction — concurrent readers run in parallel instead of queueing behind redb’s single-writer lock. 200 queries against a 1,000-node dataset, single thread vs. split across N threads sharing one Arc<Database>:

ThreadsResultSpeedup vs. 1 thread
1 (sequential)339.1 ms
4197.0 ms1.72x
8181.5 ms1.87x

Real, but sub-linear — plateaus around 4-8 threads on the 14-core machine this was measured on.

Scope of these numbers

  • No disk-backed sustained-write benchmarks — everything above ran against Database::in_memory().
  • No comparison against Neo4j, JanusGraph, Neptune, or any other graph database.

Contributing

Before opening a PR

CI (.github/workflows/rust.yml) runs on every push/PR to main:

  • Format + Clippy: cargo fmt --all -- --check and cargo clippy --workspace --all-targets -- -D warnings — zero warnings tolerated.
  • Tests: cargo test --workspace --verbose on Linux, macOS, and Windows.
  • Bindings: builds and tests the Python (maturin + unittest) and Go (go test) bindings against the same Rust workspace build.

Run the same checks locally before pushing:

cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace

Additional local checks

cargo test -p marsdb-graph --test stress -- --ignored --nocapture  # ~15s, large-scale
cargo test -p marsdb-crash-harness -- --ignored --nocapture        # ~7s/30 runs, SIGKILL-and-verify
cargo bench -p marsdb-graph
cargo bench -p marsdb

marsdb-crash-harness is a process-crash durability check (not a full power-loss test — the OS stays up, page cache intact): it spawns a child process committing one transaction at a time, SIGKILLs it at an unpredictable point, reopens the file fresh, and asserts every acknowledged commit survived intact with no gaps or duplicates.

The Cypher parser (the one part of MarsDB that takes raw, untrusted string input directly) is fuzzed via cargo-fuzz — needs nightly:

cargo install cargo-fuzz
cd marsdb-query && cargo +nightly fuzz run parse -- -max_total_time=120

Only claim: never panics. A parse error (Result::Err) is the expected, correct outcome for most fuzzer-generated input.

openCypher TCK conformance

Changes that touch marsdb-query should be checked against the real openCypher TCK, not just the crate’s own unit/smoke tests — see Cypher Language Support for what this measures and why:

git submodule update --init marsdb-tck/openCypher
cargo run --release -p marsdb-tck

For fast iteration, restrict to a category/feature:

TCK_FILTER="clauses/create" cargo run -p marsdb-tck --release

If a change moves the conformance numbers, update the table in CYPHER_COVERAGE.md (exact row numbers + the TOTAL row) in the same PR — it’s the ground truth this book’s own Cypher Language Support page is condensed from.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option. By contributing, you agree your contribution is licensed under the same terms.