#[cfg(feature = "wal")]
mod wal_directory {
use grafeo_common::types::Value;
use grafeo_engine::config::StorageFormat;
use grafeo_engine::{Config, GrafeoDB};
#[test]
fn roundtrip_no_rotation() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("testdb");
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("open for write");
let a = db.create_node(&["Person"]);
db.set_node_property(a, "name", Value::String("Alix".into()));
let b = db.create_node(&["Person"]);
db.set_node_property(b, "name", Value::String("Gus".into()));
db.create_edge(a, b, "KNOWS");
db.close().expect("close");
}
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("reopen");
assert_eq!(db.node_count(), 2, "expected 2 nodes after reopen");
assert_eq!(db.edge_count(), 1, "expected 1 edge after reopen");
db.close().expect("close");
}
}
#[test]
fn roundtrip_after_wal_rotation() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("rotdb");
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("open for write");
let a = db.create_node(&["Person"]);
db.set_node_property(a, "name", Value::String("Alix".into()));
let b = db.create_node(&["Person"]);
db.set_node_property(b, "name", Value::String("Gus".into()));
db.create_edge(a, b, "KNOWS");
let wal = db.wal().expect("WAL should be present");
wal.rotate().expect("rotate WAL");
let c = db.create_node(&["Person"]);
db.set_node_property(c, "name", Value::String("Vincent".into()));
db.close().expect("close");
}
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("reopen");
assert_eq!(
db.node_count(),
3,
"expected 3 nodes after reopen (2 before rotation + 1 after)"
);
assert_eq!(
db.edge_count(),
1,
"expected 1 edge after reopen (created before rotation)"
);
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Alix'}) RETURN n.name")
.unwrap();
assert_eq!(
result.rows().len(),
1,
"node created before WAL rotation should be recoverable"
);
db.close().expect("close");
}
}
#[test]
fn multiple_reopen_cycles() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("cycles");
let open = || {
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
GrafeoDB::with_config(config).expect("open")
};
{
let db = open();
for i in 0..5 {
db.create_node_with_props(
&["Batch"],
[("cycle", Value::Int64(1)), ("idx", Value::Int64(i))],
);
}
db.close().expect("close");
}
{
let db = open();
assert_eq!(db.node_count(), 5);
for i in 0..10 {
db.create_node_with_props(
&["Batch"],
[("cycle", Value::Int64(2)), ("idx", Value::Int64(i))],
);
}
db.wal().expect("wal").rotate().expect("rotate");
for i in 0..3 {
db.create_node_with_props(
&["Batch"],
[("cycle", Value::Int64(2)), ("idx", Value::Int64(10 + i))],
);
}
db.close().expect("close");
}
{
let db = open();
assert_eq!(db.node_count(), 18, "5 + 13 nodes across two cycles");
let session = db.session();
let result = session
.execute("MATCH (n:Batch) WHERE n.cycle = 1 RETURN count(n)")
.unwrap();
assert_eq!(result.rows()[0][0], Value::Int64(5), "cycle-1 nodes intact");
let result = session
.execute("MATCH (n:Batch) WHERE n.cycle = 2 RETURN count(n)")
.unwrap();
assert_eq!(
result.rows()[0][0],
Value::Int64(13),
"cycle-2 nodes intact"
);
db.close().expect("close");
}
}
#[test]
fn drop_without_explicit_close() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("dropdb");
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("open");
let n = db.create_node(&["Person"]);
db.set_node_property(n, "name", Value::String("Alix".into()));
}
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("reopen");
assert_eq!(db.node_count(), 1, "node should survive implicit Drop");
let session = db.session();
let result = session.execute("MATCH (n:Person) RETURN n.name").unwrap();
assert_eq!(
result.rows()[0][0],
Value::String("Alix".into()),
"property should survive implicit Drop"
);
}
}
#[test]
fn second_batch_crud_does_not_deadlock() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("deadlock_test");
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("open");
for i in 0..10 {
let id = db.create_node(&["Person"]);
db.set_node_property(id, "name", Value::from(format!("Node{i}")));
db.set_node_property(id, "index", Value::Int64(i));
}
std::thread::sleep(std::time::Duration::from_millis(200));
for i in 10..20 {
let id = db.create_node(&["Person"]);
db.set_node_property(id, "name", Value::from(format!("Node{i}")));
db.set_node_property(id, "index", Value::Int64(i));
}
assert_eq!(db.node_count(), 20);
db.close().expect("close");
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("reopen");
assert_eq!(db.node_count(), 20, "all nodes should survive reopen");
db.close().expect("close");
}
#[test]
fn no_checkpoint_meta_written() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("nockpt");
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("open");
db.create_node(&["Test"]);
db.close().expect("close");
}
let wal_dir = path.join("wal");
let checkpoint_meta = wal_dir.join("checkpoint.meta");
assert!(
!checkpoint_meta.exists(),
"checkpoint.meta should NOT exist for directory-format WAL"
);
}
#[test]
fn query_mutations_persist_directory_format() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("query_dir");
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("open");
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix', age: 30})")
.expect("insert Alix");
session
.execute("INSERT (:Person {name: 'Gus', age: 25})")
.expect("insert Gus");
session
.execute(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
INSERT (a)-[:KNOWS {since: 2020}]->(b)",
)
.expect("insert edge");
db.close().expect("close");
}
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("reopen");
assert_eq!(db.node_count(), 2, "query-created nodes should persist");
assert_eq!(db.edge_count(), 1, "query-created edge should persist");
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Alix'}) RETURN n.age")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(
result.rows()[0][0],
Value::Int64(30),
"property should survive WAL replay"
);
let result = session
.execute("MATCH ()-[e:KNOWS]->() RETURN e.since")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(
result.rows()[0][0],
Value::Int64(2020),
"edge property should survive WAL replay"
);
db.close().expect("close");
}
}
#[test]
fn edge_property_and_delete_roundtrip() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("edgeprops");
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("open");
let a = db.create_node(&["Person"]);
db.set_node_property(a, "name", Value::String("Alix".into()));
db.set_node_property(a, "temp", Value::String("remove_me".into()));
db.remove_node_property(a, "temp");
let b = db.create_node(&["Person"]);
db.set_node_property(b, "name", Value::String("Gus".into()));
let c = db.create_node(&["Person"]);
db.set_node_property(c, "name", Value::String("Vincent".into()));
let e1 = db.create_edge(a, b, "KNOWS");
db.set_edge_property(e1, "weight", Value::Float64(0.8));
let e2 = db.create_edge(b, c, "KNOWS");
db.delete_edge(e2);
db.delete_node(c);
db.close().expect("close");
}
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("reopen");
assert_eq!(db.node_count(), 2, "Vincent should be deleted");
assert_eq!(db.edge_count(), 1, "only KNOWS(Alix->Gus) should remain");
let session = db.session();
let result = session
.execute("MATCH (n:Person {name: 'Alix'}) RETURN n.temp")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(
result.rows()[0][0],
Value::Null,
"removed property should stay removed"
);
let result = session
.execute("MATCH ()-[e:KNOWS]->() RETURN e.weight")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(
result.rows()[0][0],
Value::Float64(0.8),
"edge property should persist"
);
db.close().expect("close");
}
}
#[test]
fn named_graph_persists_directory_format() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("named_graph_dir");
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("open");
let session = db.session();
session
.execute("CREATE GRAPH social")
.expect("create graph");
session.execute("USE GRAPH social").expect("use graph");
session
.execute("INSERT (:Friend {name: 'Alix'})")
.expect("insert in named graph");
session
.execute("INSERT (:Friend {name: 'Gus'})")
.expect("insert in named graph");
session.execute("USE GRAPH DEFAULT").expect("use default");
session
.execute("INSERT (:Root {tag: 'default'})")
.expect("insert in default");
db.close().expect("close");
}
{
let config = Config::persistent(&path).with_storage_format(StorageFormat::WalDirectory);
let db = GrafeoDB::with_config(config).expect("reopen");
let session = db.session();
let result = session.execute("MATCH (n:Root) RETURN n.tag").unwrap();
assert_eq!(result.rows().len(), 1, "default graph node should persist");
session.execute("USE GRAPH social").expect("use graph");
let result = session
.execute("MATCH (n:Friend) RETURN n.name ORDER BY n.name")
.unwrap();
assert_eq!(
result.rows().len(),
2,
"named graph nodes should persist across restart"
);
db.close().expect("close");
}
}
#[test]
fn auto_detect_format_recovery() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("autodetect");
{
let config = Config::persistent(&path);
let db = GrafeoDB::with_config(config).expect("open");
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix'})")
.expect("insert");
session
.execute("INSERT (:Person {name: 'Gus'})")
.expect("insert");
db.close().expect("close");
}
{
let config = Config::persistent(&path);
let db = GrafeoDB::with_config(config).expect("reopen");
assert_eq!(
db.node_count(),
2,
"auto-detected format should replay WAL on reopen"
);
let session = db.session();
let result = session
.execute("MATCH (n:Person) RETURN n.name ORDER BY n.name")
.unwrap();
assert_eq!(result.rows().len(), 2);
db.close().expect("close");
}
}
}