marsdb 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- marsdb-0.1.0/Cargo.toml +32 -0
- marsdb-0.1.0/PKG-INFO +10 -0
- marsdb-0.1.0/marsdb/Cargo.toml +26 -0
- marsdb-0.1.0/marsdb/benches/cypher_ops.rs +62 -0
- marsdb-0.1.0/marsdb/src/lib.rs +47 -0
- marsdb-0.1.0/marsdb/tests/embed.rs +36 -0
- marsdb-0.1.0/marsdb-graph/Cargo.toml +23 -0
- marsdb-0.1.0/marsdb-graph/benches/graph_ops.rs +109 -0
- marsdb-0.1.0/marsdb-graph/src/encode.rs +26 -0
- marsdb-0.1.0/marsdb-graph/src/error.rs +17 -0
- marsdb-0.1.0/marsdb-graph/src/id.rs +15 -0
- marsdb-0.1.0/marsdb-graph/src/labels.rs +47 -0
- marsdb-0.1.0/marsdb-graph/src/lib.rs +11 -0
- marsdb-0.1.0/marsdb-graph/src/model.rs +68 -0
- marsdb-0.1.0/marsdb-graph/src/store.rs +398 -0
- marsdb-0.1.0/marsdb-graph/tests/abort_probe.rs +28 -0
- marsdb-0.1.0/marsdb-graph/tests/smoke.rs +65 -0
- marsdb-0.1.0/marsdb-graph/tests/stress.rs +131 -0
- marsdb-0.1.0/marsdb-python/Cargo.lock +440 -0
- marsdb-0.1.0/marsdb-python/Cargo.toml +17 -0
- marsdb-0.1.0/marsdb-python/src/lib.rs +107 -0
- marsdb-0.1.0/marsdb-query/Cargo.toml +14 -0
- marsdb-0.1.0/marsdb-query/src/ast.rs +95 -0
- marsdb-0.1.0/marsdb-query/src/cypher.pest +56 -0
- marsdb-0.1.0/marsdb-query/src/error.rs +9 -0
- marsdb-0.1.0/marsdb-query/src/executor.rs +381 -0
- marsdb-0.1.0/marsdb-query/src/ir.rs +42 -0
- marsdb-0.1.0/marsdb-query/src/lib.rs +15 -0
- marsdb-0.1.0/marsdb-query/src/parser.rs +289 -0
- marsdb-0.1.0/marsdb-query/src/planner.rs +92 -0
- marsdb-0.1.0/marsdb-query/src/result.rs +7 -0
- marsdb-0.1.0/marsdb-query/src/value.rs +12 -0
- marsdb-0.1.0/marsdb-query/tests/smoke.rs +96 -0
- marsdb-0.1.0/marsdb-storage/Cargo.toml +12 -0
- marsdb-0.1.0/marsdb-storage/src/error.rs +54 -0
- marsdb-0.1.0/marsdb-storage/src/lib.rs +65 -0
- marsdb-0.1.0/marsdb-storage/src/tables.rs +22 -0
- marsdb-0.1.0/pyproject.toml +21 -0
marsdb-0.1.0/Cargo.toml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
resolver = "2"
|
|
3
|
+
members = [
|
|
4
|
+
"marsdb-storage",
|
|
5
|
+
"marsdb-graph",
|
|
6
|
+
"marsdb-query",
|
|
7
|
+
"marsdb",
|
|
8
|
+
"marsdb-cli",
|
|
9
|
+
]
|
|
10
|
+
# marsdb-python builds as a PyO3 extension-module (no libpython link, no
|
|
11
|
+
# runnable test binary) via maturin, not plain `cargo build`/`cargo test` —
|
|
12
|
+
# kept out of the workspace so it doesn't affect `cargo test --workspace`.
|
|
13
|
+
exclude = ["marsdb-python"]
|
|
14
|
+
|
|
15
|
+
[workspace.package]
|
|
16
|
+
version = "0.1.0"
|
|
17
|
+
edition = "2021"
|
|
18
|
+
license = "MIT OR Apache-2.0"
|
|
19
|
+
repository = "https://github.com/knoguchi/marsdb"
|
|
20
|
+
homepage = "https://github.com/knoguchi/marsdb"
|
|
21
|
+
|
|
22
|
+
[workspace.dependencies]
|
|
23
|
+
redb = "2"
|
|
24
|
+
serde = { version = "1", features = ["derive"] }
|
|
25
|
+
postcard = { version = "1", features = ["alloc"] }
|
|
26
|
+
thiserror = "1"
|
|
27
|
+
pest = "2"
|
|
28
|
+
pest_derive = "2"
|
|
29
|
+
clap = { version = "4", features = ["derive"] }
|
|
30
|
+
rustyline = "14"
|
|
31
|
+
criterion = "0.5"
|
|
32
|
+
tempfile = "3"
|
marsdb-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: marsdb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Programming Language :: Rust
|
|
5
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
|
+
Summary: Embeddable property-graph database with an openCypher query subset.
|
|
7
|
+
Home-Page: https://github.com/knoguchi/marsdb
|
|
8
|
+
License: MIT OR Apache-2.0
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Project-URL: Repository, https://github.com/knoguchi/marsdb
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "marsdb"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
license.workspace = true
|
|
6
|
+
repository.workspace = true
|
|
7
|
+
homepage.workspace = true
|
|
8
|
+
description = "Embeddable property-graph database with an openCypher query subset: single binary, single file, optional in-memory mode."
|
|
9
|
+
|
|
10
|
+
[lib]
|
|
11
|
+
name = "marsdb"
|
|
12
|
+
path = "src/lib.rs"
|
|
13
|
+
|
|
14
|
+
[dependencies]
|
|
15
|
+
marsdb-storage = { path = "../marsdb-storage", version = "0.1.0" }
|
|
16
|
+
marsdb-graph = { path = "../marsdb-graph", version = "0.1.0" }
|
|
17
|
+
marsdb-query = { path = "../marsdb-query", version = "0.1.0" }
|
|
18
|
+
thiserror.workspace = true
|
|
19
|
+
|
|
20
|
+
[dev-dependencies]
|
|
21
|
+
tempfile = "3"
|
|
22
|
+
criterion.workspace = true
|
|
23
|
+
|
|
24
|
+
[[bench]]
|
|
25
|
+
name = "cypher_ops"
|
|
26
|
+
harness = false
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
|
2
|
+
use marsdb::Database;
|
|
3
|
+
|
|
4
|
+
fn make_chain_create(hops: usize) -> String {
|
|
5
|
+
let mut chain = "(n0:Item {idx: 0})".to_string();
|
|
6
|
+
for i in 1..=hops {
|
|
7
|
+
chain.push_str(&format!("-[:R]->(n{i}:Item {{idx: {i}}})"));
|
|
8
|
+
}
|
|
9
|
+
format!("CREATE {chain}")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// Isolates parsing cost from execution cost — answers the "where does the
|
|
13
|
+
/// time go" question cleanly, in-process (no subprocess/CLI confounders).
|
|
14
|
+
fn bench_parse_only(c: &mut Criterion) {
|
|
15
|
+
let mut group = c.benchmark_group("parse_only_by_hop_count");
|
|
16
|
+
for hops in [10usize, 100, 1_000] {
|
|
17
|
+
let cypher = make_chain_create(hops);
|
|
18
|
+
group.bench_with_input(BenchmarkId::from_parameter(hops), &hops, |b, _| {
|
|
19
|
+
b.iter(|| black_box(marsdb_query::parse(&cypher).unwrap()));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
group.finish();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn bench_execute_create(c: &mut Criterion) {
|
|
26
|
+
let mut group = c.benchmark_group("execute_create_by_hop_count");
|
|
27
|
+
for hops in [10usize, 100, 1_000] {
|
|
28
|
+
let cypher = make_chain_create(hops);
|
|
29
|
+
group.bench_with_input(BenchmarkId::from_parameter(hops), &hops, |b, _| {
|
|
30
|
+
b.iter(|| {
|
|
31
|
+
let db = Database::in_memory().unwrap();
|
|
32
|
+
black_box(db.execute(&cypher).unwrap())
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
group.finish();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn bench_execute_match_1hop(c: &mut Criterion) {
|
|
40
|
+
let mut group = c.benchmark_group("execute_match_1hop_by_dataset_size");
|
|
41
|
+
for n in [100usize, 1_000, 10_000] {
|
|
42
|
+
let db = Database::in_memory().unwrap();
|
|
43
|
+
db.execute(&make_chain_create(n)).unwrap();
|
|
44
|
+
group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, _| {
|
|
45
|
+
b.iter(|| {
|
|
46
|
+
black_box(
|
|
47
|
+
db.execute("MATCH (n:Item)-[:R]->(m:Item) RETURN m.idx LIMIT 10")
|
|
48
|
+
.unwrap(),
|
|
49
|
+
)
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
group.finish();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
criterion_group!(
|
|
57
|
+
benches,
|
|
58
|
+
bench_parse_only,
|
|
59
|
+
bench_execute_create,
|
|
60
|
+
bench_execute_match_1hop,
|
|
61
|
+
);
|
|
62
|
+
criterion_main!(benches);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//! Embeddable property-graph database with an openCypher query subset:
|
|
2
|
+
//! single binary, single file, optional in-memory mode.
|
|
3
|
+
//!
|
|
4
|
+
//! ```
|
|
5
|
+
//! let db = marsdb::Database::in_memory().unwrap();
|
|
6
|
+
//! db.execute("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})").unwrap();
|
|
7
|
+
//! let result = db.execute("MATCH (n:Person) RETURN n.name").unwrap();
|
|
8
|
+
//! assert_eq!(result.rows.len(), 2);
|
|
9
|
+
//! ```
|
|
10
|
+
|
|
11
|
+
use std::path::Path;
|
|
12
|
+
|
|
13
|
+
pub use marsdb_query::{Literal, QueryResult, Value};
|
|
14
|
+
|
|
15
|
+
#[derive(Debug, thiserror::Error)]
|
|
16
|
+
pub enum Error {
|
|
17
|
+
#[error("graph error: {0}")]
|
|
18
|
+
Graph(#[from] marsdb_graph::GraphError),
|
|
19
|
+
#[error("query error: {0}")]
|
|
20
|
+
Query(#[from] marsdb_query::QueryError),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub struct Database {
|
|
24
|
+
store: marsdb_graph::GraphStore,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl Database {
|
|
28
|
+
/// Open (creating if absent) a single-file, on-disk database.
|
|
29
|
+
pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
|
|
30
|
+
Ok(Self {
|
|
31
|
+
store: marsdb_graph::GraphStore::open_file(path)?,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Open a purely in-memory database. Nothing is written to disk.
|
|
36
|
+
pub fn in_memory() -> Result<Self, Error> {
|
|
37
|
+
Ok(Self {
|
|
38
|
+
store: marsdb_graph::GraphStore::open_memory()?,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn execute(&self, cypher: &str) -> Result<QueryResult, Error> {
|
|
43
|
+
let stmt = marsdb_query::parse(cypher)?;
|
|
44
|
+
let result = marsdb_query::Executor::new(&self.store).execute(&stmt)?;
|
|
45
|
+
Ok(result)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//! Proves the crate is embeddable independent of the CLI subprocess.
|
|
2
|
+
|
|
3
|
+
use marsdb::Database;
|
|
4
|
+
|
|
5
|
+
#[test]
|
|
6
|
+
fn in_memory_roundtrip() {
|
|
7
|
+
let db = Database::in_memory().unwrap();
|
|
8
|
+
db.execute("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})")
|
|
9
|
+
.unwrap();
|
|
10
|
+
let result = db.execute("MATCH (n:Person) RETURN n.name").unwrap();
|
|
11
|
+
assert_eq!(result.rows.len(), 2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[test]
|
|
15
|
+
fn file_backed_persists_after_reopen() {
|
|
16
|
+
let dir = tempfile::tempdir().unwrap();
|
|
17
|
+
let path = dir.path().join("test.db");
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
let db = Database::open(&path).unwrap();
|
|
21
|
+
db.execute("CREATE (a:Person {name: 'Alice'})").unwrap();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let db = Database::open(&path).unwrap();
|
|
25
|
+
let result = db.execute("MATCH (n:Person) RETURN n.name").unwrap();
|
|
26
|
+
assert_eq!(result.rows.len(), 1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[test]
|
|
30
|
+
fn two_in_memory_databases_do_not_share_data() {
|
|
31
|
+
let a = Database::in_memory().unwrap();
|
|
32
|
+
let b = Database::in_memory().unwrap();
|
|
33
|
+
a.execute("CREATE (n:Person {name: 'Alice'})").unwrap();
|
|
34
|
+
let result = b.execute("MATCH (n:Person) RETURN n.name").unwrap();
|
|
35
|
+
assert_eq!(result.rows.len(), 0);
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "marsdb-graph"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
license.workspace = true
|
|
6
|
+
repository.workspace = true
|
|
7
|
+
homepage.workspace = true
|
|
8
|
+
description = "Property graph model and CRUD storage layer used internally by MarsDB."
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
marsdb-storage = { path = "../marsdb-storage", version = "0.1.0" }
|
|
12
|
+
redb.workspace = true
|
|
13
|
+
serde.workspace = true
|
|
14
|
+
postcard.workspace = true
|
|
15
|
+
thiserror.workspace = true
|
|
16
|
+
|
|
17
|
+
[dev-dependencies]
|
|
18
|
+
tempfile = "3"
|
|
19
|
+
criterion.workspace = true
|
|
20
|
+
|
|
21
|
+
[[bench]]
|
|
22
|
+
name = "graph_ops"
|
|
23
|
+
harness = false
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
use std::collections::BTreeMap;
|
|
2
|
+
|
|
3
|
+
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
|
|
4
|
+
use marsdb_graph::{Direction, GraphStore, PropertyValue};
|
|
5
|
+
|
|
6
|
+
fn bench_create_node(c: &mut Criterion) {
|
|
7
|
+
let store = GraphStore::open_memory().unwrap();
|
|
8
|
+
c.bench_function("create_node", |b| {
|
|
9
|
+
b.iter(|| {
|
|
10
|
+
let mut props = BTreeMap::new();
|
|
11
|
+
props.insert("name".to_string(), PropertyValue::String("x".to_string()));
|
|
12
|
+
black_box(store.create_node("Item", props).unwrap())
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn bench_create_edge(c: &mut Criterion) {
|
|
18
|
+
let store = GraphStore::open_memory().unwrap();
|
|
19
|
+
let a = store.create_node("Item", BTreeMap::new()).unwrap();
|
|
20
|
+
let b_node = store.create_node("Item", BTreeMap::new()).unwrap();
|
|
21
|
+
c.bench_function("create_edge", |b| {
|
|
22
|
+
b.iter(|| black_box(store.create_edge("REL", a, b_node, BTreeMap::new()).unwrap()));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn bench_get_node(c: &mut Criterion) {
|
|
27
|
+
let store = GraphStore::open_memory().unwrap();
|
|
28
|
+
let id = store.create_node("Item", BTreeMap::new()).unwrap();
|
|
29
|
+
c.bench_function("get_node", |b| {
|
|
30
|
+
b.iter(|| black_box(store.get_node(id).unwrap()));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn bench_neighbors_1hop(c: &mut Criterion) {
|
|
35
|
+
let mut group = c.benchmark_group("neighbors_1hop_by_fanout");
|
|
36
|
+
for fanout in [1u64, 10, 100, 1_000] {
|
|
37
|
+
let store = GraphStore::open_memory().unwrap();
|
|
38
|
+
let center = store.create_node("Item", BTreeMap::new()).unwrap();
|
|
39
|
+
for _ in 0..fanout {
|
|
40
|
+
let n = store.create_node("Item", BTreeMap::new()).unwrap();
|
|
41
|
+
store.create_edge("REL", center, n, BTreeMap::new()).unwrap();
|
|
42
|
+
}
|
|
43
|
+
group.bench_with_input(BenchmarkId::from_parameter(fanout), &fanout, |b, _| {
|
|
44
|
+
b.iter(|| black_box(store.neighbors(center, Direction::Out, None).unwrap()));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
group.finish();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn bench_all_nodes_scan(c: &mut Criterion) {
|
|
51
|
+
let mut group = c.benchmark_group("all_nodes_scan_by_table_size");
|
|
52
|
+
for n in [100i64, 1_000, 10_000] {
|
|
53
|
+
let store = GraphStore::open_memory().unwrap();
|
|
54
|
+
for i in 0..n {
|
|
55
|
+
let mut props = BTreeMap::new();
|
|
56
|
+
props.insert("idx".to_string(), PropertyValue::Int(i));
|
|
57
|
+
store.create_node("Item", props).unwrap();
|
|
58
|
+
}
|
|
59
|
+
group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, _| {
|
|
60
|
+
b.iter(|| black_box(store.all_nodes(Some("Item")).unwrap()));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
group.finish();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Follow-up on an earlier open question: does batching many writes into
|
|
67
|
+
/// one transaction (via the `*_in_txn` API the query executor uses) matter
|
|
68
|
+
/// versus one transaction per node? Answers it directly instead of guessing.
|
|
69
|
+
fn bench_bulk_create_txn_strategy(c: &mut Criterion) {
|
|
70
|
+
const N: i64 = 1_000;
|
|
71
|
+
let mut group = c.benchmark_group("bulk_create_1000_nodes");
|
|
72
|
+
|
|
73
|
+
group.bench_function("one_txn_per_node", |b| {
|
|
74
|
+
b.iter(|| {
|
|
75
|
+
let store = GraphStore::open_memory().unwrap();
|
|
76
|
+
for i in 0..N {
|
|
77
|
+
let mut props = BTreeMap::new();
|
|
78
|
+
props.insert("idx".to_string(), PropertyValue::Int(i));
|
|
79
|
+
store.create_node("Item", props).unwrap();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
group.bench_function("one_txn_total", |b| {
|
|
85
|
+
b.iter(|| {
|
|
86
|
+
let store = GraphStore::open_memory().unwrap();
|
|
87
|
+
let write_txn = store.begin_write().unwrap();
|
|
88
|
+
for i in 0..N {
|
|
89
|
+
let mut props = BTreeMap::new();
|
|
90
|
+
props.insert("idx".to_string(), PropertyValue::Int(i));
|
|
91
|
+
GraphStore::create_node_in_txn(&write_txn, "Item", props).unwrap();
|
|
92
|
+
}
|
|
93
|
+
GraphStore::commit(write_txn).unwrap();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
group.finish();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
criterion_group!(
|
|
101
|
+
benches,
|
|
102
|
+
bench_create_node,
|
|
103
|
+
bench_create_edge,
|
|
104
|
+
bench_get_node,
|
|
105
|
+
bench_neighbors_1hop,
|
|
106
|
+
bench_all_nodes_scan,
|
|
107
|
+
bench_bulk_create_txn_strategy,
|
|
108
|
+
);
|
|
109
|
+
criterion_main!(benches);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
use std::collections::BTreeMap;
|
|
2
|
+
|
|
3
|
+
use crate::error::GraphError;
|
|
4
|
+
use crate::model::PropertyValue;
|
|
5
|
+
|
|
6
|
+
#[derive(serde::Serialize, serde::Deserialize)]
|
|
7
|
+
pub(crate) struct NodeRecord {
|
|
8
|
+
pub label_id: u32,
|
|
9
|
+
pub props: BTreeMap<String, PropertyValue>,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#[derive(serde::Serialize, serde::Deserialize)]
|
|
13
|
+
pub(crate) struct EdgeRecord {
|
|
14
|
+
pub label_id: u32,
|
|
15
|
+
pub src: u64,
|
|
16
|
+
pub dst: u64,
|
|
17
|
+
pub props: BTreeMap<String, PropertyValue>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub(crate) fn encode<T: serde::Serialize>(value: &T) -> Result<Vec<u8>, GraphError> {
|
|
21
|
+
Ok(postcard::to_allocvec(value)?)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub(crate) fn decode<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T, GraphError> {
|
|
25
|
+
Ok(postcard::from_bytes(bytes)?)
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
use crate::model::NodeId;
|
|
2
|
+
|
|
3
|
+
#[derive(Debug, thiserror::Error)]
|
|
4
|
+
pub enum GraphError {
|
|
5
|
+
#[error("storage error: {0}")]
|
|
6
|
+
Storage(#[from] marsdb_storage::StorageError),
|
|
7
|
+
#[error("table error: {0}")]
|
|
8
|
+
Table(#[from] redb::TableError),
|
|
9
|
+
#[error("storage error: {0}")]
|
|
10
|
+
RedbStorage(#[from] redb::StorageError),
|
|
11
|
+
#[error("commit error: {0}")]
|
|
12
|
+
Commit(#[from] redb::CommitError),
|
|
13
|
+
#[error("encode error: {0}")]
|
|
14
|
+
Encode(#[from] postcard::Error),
|
|
15
|
+
#[error("node {0:?} has incident edges; use detach delete")]
|
|
16
|
+
NodeHasEdges(NodeId),
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
use marsdb_storage::{ReadableTable, WriteTransaction};
|
|
2
|
+
|
|
3
|
+
use crate::error::GraphError;
|
|
4
|
+
|
|
5
|
+
/// Increment and return the counter stored at `key` in the `meta` table,
|
|
6
|
+
/// within the given write transaction. Allocation is only durable once the
|
|
7
|
+
/// caller commits `write_txn`, keeping id allocation inside the same
|
|
8
|
+
/// crash-safety boundary as the row(s) it's used for.
|
|
9
|
+
pub(crate) fn next_id(write_txn: &WriteTransaction, key: &str) -> Result<u64, GraphError> {
|
|
10
|
+
let mut meta = write_txn.open_table(marsdb_storage::tables::META)?;
|
|
11
|
+
let current = meta.get(key)?.map(|g| g.value()).unwrap_or(0);
|
|
12
|
+
let next = current + 1;
|
|
13
|
+
meta.insert(key, next)?;
|
|
14
|
+
Ok(next)
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
use marsdb_storage::{ReadableTable, WriteTransaction};
|
|
2
|
+
|
|
3
|
+
use crate::error::GraphError;
|
|
4
|
+
use crate::id::next_id;
|
|
5
|
+
|
|
6
|
+
/// Look up (or allocate) the u32 id interned for `label`, inside a write txn.
|
|
7
|
+
pub(crate) fn intern_label(write_txn: &WriteTransaction, label: &str) -> Result<u32, GraphError> {
|
|
8
|
+
{
|
|
9
|
+
let l2i = write_txn.open_table(marsdb_storage::tables::LABEL_TO_ID)?;
|
|
10
|
+
let existing = l2i.get(label)?.map(|g| g.value());
|
|
11
|
+
if let Some(existing) = existing {
|
|
12
|
+
return Ok(existing);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
let id = next_id(write_txn, "next_label_id")? as u32;
|
|
16
|
+
{
|
|
17
|
+
let mut l2i = write_txn.open_table(marsdb_storage::tables::LABEL_TO_ID)?;
|
|
18
|
+
l2i.insert(label, id)?;
|
|
19
|
+
}
|
|
20
|
+
{
|
|
21
|
+
let mut i2l = write_txn.open_table(marsdb_storage::tables::ID_TO_LABEL)?;
|
|
22
|
+
i2l.insert(id, label)?;
|
|
23
|
+
}
|
|
24
|
+
Ok(id)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Resolve a previously interned label id back to its string.
|
|
28
|
+
///
|
|
29
|
+
/// Takes `&WriteTransaction` (not `&ReadTransaction`) even for read-only
|
|
30
|
+
/// callers: v1 drives an entire statement through one `WriteTransaction` —
|
|
31
|
+
/// see `GraphStore::begin_write` — so every table access in this crate goes
|
|
32
|
+
/// through that one transaction type, read or write.
|
|
33
|
+
pub(crate) fn resolve_label(write_txn: &WriteTransaction, label_id: u32) -> Result<String, GraphError> {
|
|
34
|
+
let i2l = write_txn.open_table(marsdb_storage::tables::ID_TO_LABEL)?;
|
|
35
|
+
let value = i2l
|
|
36
|
+
.get(label_id)?
|
|
37
|
+
.expect("label id present in nodes/edges table must be interned");
|
|
38
|
+
Ok(value.value().to_string())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Look up the id for `label` without allocating one. Returns `None` if the
|
|
42
|
+
/// label has never been used, meaning no rows can reference it.
|
|
43
|
+
pub(crate) fn lookup_label_id(write_txn: &WriteTransaction, label: &str) -> Result<Option<u32>, GraphError> {
|
|
44
|
+
let l2i = write_txn.open_table(marsdb_storage::tables::LABEL_TO_ID)?;
|
|
45
|
+
let found = l2i.get(label)?.map(|g| g.value());
|
|
46
|
+
Ok(found)
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
mod encode;
|
|
2
|
+
mod error;
|
|
3
|
+
mod id;
|
|
4
|
+
mod labels;
|
|
5
|
+
mod model;
|
|
6
|
+
mod store;
|
|
7
|
+
|
|
8
|
+
pub use error::GraphError;
|
|
9
|
+
pub use marsdb_storage::WriteTransaction;
|
|
10
|
+
pub use model::{AdjEntry, Direction, Edge, EdgeId, Node, NodeId, PropertyValue};
|
|
11
|
+
pub use store::GraphStore;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
use std::collections::BTreeMap;
|
|
2
|
+
|
|
3
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
4
|
+
pub struct NodeId(pub u64);
|
|
5
|
+
|
|
6
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
7
|
+
pub struct EdgeId(pub u64);
|
|
8
|
+
|
|
9
|
+
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
10
|
+
pub enum PropertyValue {
|
|
11
|
+
Null,
|
|
12
|
+
Bool(bool),
|
|
13
|
+
Int(i64),
|
|
14
|
+
Float(f64),
|
|
15
|
+
String(String),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
19
|
+
pub struct Node {
|
|
20
|
+
pub id: NodeId,
|
|
21
|
+
pub label: String,
|
|
22
|
+
pub props: BTreeMap<String, PropertyValue>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
26
|
+
pub struct Edge {
|
|
27
|
+
pub id: EdgeId,
|
|
28
|
+
pub label: String,
|
|
29
|
+
pub src: NodeId,
|
|
30
|
+
pub dst: NodeId,
|
|
31
|
+
pub props: BTreeMap<String, PropertyValue>,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
35
|
+
pub enum Direction {
|
|
36
|
+
Out,
|
|
37
|
+
In,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// A traversal-hop candidate read directly from an adjacency multimap entry,
|
|
41
|
+
/// without touching the `edges`/`nodes` tables.
|
|
42
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
43
|
+
pub struct AdjEntry {
|
|
44
|
+
pub edge_id: EdgeId,
|
|
45
|
+
pub other: NodeId,
|
|
46
|
+
pub label_id: u32,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
impl AdjEntry {
|
|
50
|
+
pub(crate) fn encode(&self) -> [u8; 20] {
|
|
51
|
+
let mut buf = [0u8; 20];
|
|
52
|
+
buf[0..8].copy_from_slice(&self.edge_id.0.to_be_bytes());
|
|
53
|
+
buf[8..16].copy_from_slice(&self.other.0.to_be_bytes());
|
|
54
|
+
buf[16..20].copy_from_slice(&self.label_id.to_be_bytes());
|
|
55
|
+
buf
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub(crate) fn decode(bytes: &[u8]) -> Self {
|
|
59
|
+
let edge_id = u64::from_be_bytes(bytes[0..8].try_into().unwrap());
|
|
60
|
+
let other = u64::from_be_bytes(bytes[8..16].try_into().unwrap());
|
|
61
|
+
let label_id = u32::from_be_bytes(bytes[16..20].try_into().unwrap());
|
|
62
|
+
AdjEntry {
|
|
63
|
+
edge_id: EdgeId(edge_id),
|
|
64
|
+
other: NodeId(other),
|
|
65
|
+
label_id,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|