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")orDatabase::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-storageruns on redb, a pure-Rust MVCC single-file engine, and aSIGKILL-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
- New to MarsDB? Start with Install & CLI, or jump straight to Embedding in Rust if you’re integrating it into a program.
- Wondering exactly which Cypher features are supported? See Cypher Language Support.
- Curious how it’s built? See Architecture and Benchmarks.
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/Close — execute_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
| category | total | pass | pass % |
|---|---|---|---|
| clauses/call | 52 | 52 | 100.0% |
| clauses/create | 78 | 78 | 100.0% |
| clauses/delete | 41 | 41 | 100.0% |
| clauses/match | 381 | 381 | 100.0% |
| clauses/match-where | 34 | 34 | 100.0% |
| clauses/merge | 75 | 75 | 100.0% |
| clauses/remove | 33 | 33 | 100.0% |
| clauses/return | 63 | 63 | 100.0% |
| clauses/return-orderby | 35 | 35 | 100.0% |
| clauses/return-skip-limit | 31 | 31 | 100.0% |
| clauses/set | 53 | 53 | 100.0% |
| clauses/union | 12 | 12 | 100.0% |
| clauses/unwind | 14 | 14 | 100.0% |
| clauses/with | 29 | 29 | 100.0% |
| clauses/with-orderBy | 292 | 292 | 100.0% |
| clauses/with-skip-limit | 9 | 9 | 100.0% |
| clauses/with-where | 19 | 19 | 100.0% |
| expressions/aggregation | 35 | 35 | 100.0% |
| expressions/boolean | 150 | 150 | 100.0% |
| expressions/comparison | 72 | 72 | 100.0% |
| expressions/conditional | 13 | 13 | 100.0% |
| expressions/existentialSubqueries | 10 | 10 | 100.0% |
| expressions/graph | 61 | 61 | 100.0% |
| expressions/list | 185 | 185 | 100.0% |
| expressions/literals | 131 | 131 | 100.0% |
| expressions/map | 44 | 44 | 100.0% |
| expressions/mathematical | 6 | 6 | 100.0% |
| expressions/null | 44 | 44 | 100.0% |
| expressions/path | 7 | 7 | 100.0% |
| expressions/pattern | 50 | 50 | 100.0% |
| expressions/precedence | 104 | 104 | 100.0% |
| expressions/quantifier | 604 | 604 | 100.0% |
| expressions/string | 32 | 32 | 100.0% |
| expressions/temporal* | 1004 | 1002 | 99.8% |
| expressions/typeConversion | 47 | 47 | 100.0% |
| useCases/countingSubgraphMatches | 11 | 11 | 100.0% |
| useCases/triadicSelection | 19 | 19 | 100.0% |
| TOTAL | 3880 | 3878 | 99.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 nestedexists {}),UNWIND, any number of chainedWITHboundaries,ORDER BY/SKIP/LIMIT,DISTINCT. - Writing:
CREATE,MERGE(withON CREATE/ON MATCH),SET,REMOVE,DELETE/DETACH DELETE,MATCH ... CREATE, arbitrary chaining of mutating clauses via trailingWITHorRETURN.MERGEis capped at one relationship hop. - Aggregation: implicit
GROUP BY,count/sum/avg/min/max/collect/percentileCont/percentileDisc,DISTINCTinside 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-suppliedProcedureProvider— see Embedding in Rust. MarsDB itself ships no built-in procedures. - Parameters:
$namescalars, 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
$parametersaren’t supported (a map-valued parameter is). - No cost-based query optimizer — index seeks and top-k
ORDER BY ... LIMITselection exist, but join/traversal ordering isn’t cost-estimated. See Architecture. CALLneeds an embedder-suppliedProcedureProvider— 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
| Operation | Result |
|---|---|
create_node | 37.1 µs |
create_edge | 50.6 µs |
get_node (point lookup by id) | 832 ns |
neighbors, 1-hop, fanout 10 | 1.05 µs |
neighbors, 1-hop, fanout 1,000 | 54.9 µs |
all_nodes scan, label matches 1% of rows, 100,000 rows | 801 µ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
| Operation | Result |
|---|---|
Parse + execute, 10-hop CREATE | 3.58 ms |
MATCH (n)-[:R]->(m) RETURN m.idx LIMIT 10, 10,000-node dataset | 1.284 ms |
MATCH (n:Label) RETURN n LIMIT 10 (no hop/WHERE/ORDER BY), 100,000-node dataset | 22.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 size | Unindexed scan | Index seek | Speedup |
|---|---|---|---|
| 100 | 78.6 µs | 7.36 µs | 10.7x |
| 10,000 | 8.43 ms | 7.87 µs | 1,071x |
| 100,000 | 92.4 ms | 7.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:
| Operation | Result (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>:
| Threads | Result | Speedup vs. 1 thread |
|---|---|---|
| 1 (sequential) | 339.1 ms | — |
| 4 | 197.0 ms | 1.72x |
| 8 | 181.5 ms | 1.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 -- --checkandcargo clippy --workspace --all-targets -- -D warnings— zero warnings tolerated. - Tests:
cargo test --workspace --verboseon 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.