MarsDB
An embeddable property-graph database with an openCypher query subset: single binary, single file, optional in-memory mode.
$ 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
Install
CLI — installs the marsdb binary:
cargo install marsdb-cli
Rust library:
cargo add marsdb
let db = in_memory?; // or Database::open("path/to.db")
db.execute?;
let result = db.execute?;
// Or run a `;`-separated batch, one transaction per statement, one
// QueryResult per statement back:
let results = db.execute_batch?;
More: cargo run -p marsdb --example task_tracker (CRUD + aggregation),
--example social_graph (variable-length traversal, MATCH...CREATE), or
--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.
Python:
pip install marsdb
= # or .open(path)
# -> [{'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. To build from source directly:
cd marsdb-python
python3 -m venv .venv && source .venv/bin/activate
pip install maturin && maturin develop
CLI 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
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: pest 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
Storage runs on redb, a pure-Rust
single-file MVCC embedded KV engine. Query execution compiles Cypher to a
small Gremlin-shaped logical IR (AllNodesScan, NodeByLabelScan,
Seed, Expand, VarExpand, Filter) so a future Gremlin frontend can
target the same executor. 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.
Numbers: BENCHMARKS.md.
Cypher coverage
CREATE, multi-label nodes ((n:Post:Message)), $parameters,
backslash-escaped string literals (\' \" \\ \n \r \t \b \f),
MATCH/OPTIONAL MATCH, undirected (-[r:TYPE]-) and variable-length
([:TYPE*min..max]) relationship patterns, WHERE, one WITH boundary
per statement (projection/rename, its own WHERE/WITH...WHERE/ORDER BY/LIMIT), RETURN/DELETE/DETACH DELETE/SET/MATCH ... CREATE
(adds an edge between two already-matched nodes — a node token whose
variable is already bound reuses that node instead of creating a new
one), multi-key ORDER BY, LIMIT, CASE, the built-in functions
coalesce()/toInteger(), and implicit-GROUP-BY aggregation
(count()/count(*)/sum()/avg()/min()/max()/collect(), with
DISTINCT — inside an aggregate call only; a standalone RETURN DISTINCT
result-set modifier doesn't exist yet). Two independent MATCH parts
across one WITH boundary (MATCH (a) WITH a MATCH (b) ..., where b's
pattern doesn't chain from a) correctly cross-join, carrying a
alongside every row b produces. UNWIND <list> AS x (fans a list out
into one row per element, cross-joined against existing rows; its own
WHERE works without needing a second WITH) — <list> is an inline
Cypher-text list literal ([1, 2, 'a', $p]) or a variable bound by a
preceding WITH ... collect(...); UNWIND $param where $param itself
names a list isn't supported yet (no list-valued parameters — every
$param is a single scalar). MERGE <pattern> [ON CREATE SET ...] [ON MATCH SET ...] (match-or-create: tries the pattern as an ordinary MATCH
first, creates exactly one new instance if nothing matched) — capped at
one relationship hop (MERGE (n:Label {props}) or MERGE (a)-[:TYPE]-> (b)); an unconstrained node pattern that isn't already bound (MERGE (n), no label or property) is rejected rather than matching/creating
arbitrarily. Named-path capture (MATCH p = (a)-[:KNOWS]->(b) RETURN p,
fixed-hop patterns only) and shortestPath((a)-[:TYPE*..N]-(b)) (real
shortest-path search via BFS, not just the first path found — both
endpoints must already be matched by a preceding clause), plus
length(p) to measure one.
Verified against all 7 of LDBC SNB Interactive's short-read reference
queries (IS1-IS7) — see marsdb-query/tests/ldbc_is_queries.rs. Not
verified: LDBC's complex queries (IC1-14: the full query set beyond one
hand-crafted grouping+WITH...WHERE+ORDER BY+LIMIT+collect()
checkpoint — see marsdb-query/tests/smoke.rs), comma-separated patterns
within one MATCH/CREATE clause beyond a single linear chain (general
cross-joins — different from the cross-join WITH-chaining above, which
works), chaining past one WITH boundary, MERGE patterns with more than
one relationship hop (whole-pattern atomicity across multiple
simultaneously-unbound hops isn't attempted), named-path capture over a
variable-length pattern (only shortestPath() tracks the hop-by-hop chain
needed to reconstruct a path over *-traversal), or shortestPath() with
a minimum hop count greater than 1 (a plain visited-set BFS can't
correctly answer "shortest path of at least N hops" for N > 1 without a
different algorithm).
Roadmap
LIMITshort-circuitingRETURN DISTINCT(result-set-level dedup;DISTINCTinside an aggregate call already works)- List-valued
$parameters, to unblockUNWIND $items AS x - From-scratch storage engine (page format, B-tree, crash recovery) as an
alternate
marsdb-storagebackend, independent of redb - Gremlin frontend targeting the existing IR
Testing
cargo test --workspace # ~1s
cargo test -p marsdb-graph --test stress -- --ignored --nocapture # ~15s, large-scale
cargo bench -p marsdb-graph
cargo bench -p marsdb
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.