#![cfg(feature = "grafeo-file")]
use grafeo_common::types::Value;
use grafeo_engine::{Config, GrafeoDB};
fn extract_strings(rows: &[Vec<Value>]) -> Vec<String> {
let mut names: Vec<String> = rows
.iter()
.filter_map(|r| match &r[0] {
Value::String(s) => Some(s.to_string()),
_ => None,
})
.collect();
names.sort();
names
}
fn sidecar_wal_path(db_path: &std::path::Path) -> std::path::PathBuf {
let mut p = db_path.as_os_str().to_owned();
p.push(".wal");
std::path::PathBuf::from(p)
}
#[test]
fn create_new_grafeo_file() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("test.grafeo");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert_eq!(db.node_count(), 0);
assert_eq!(db.edge_count(), 0);
assert!(path.exists());
assert!(path.is_file());
db.close().unwrap();
}
#[test]
fn insert_close_reopen_persists_data() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("persist.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
session
.execute("INSERT (:Person {name: 'Gus', age: 25})")
.unwrap();
session
.execute(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
INSERT (a)-[:KNOWS]->(b)",
)
.unwrap();
assert_eq!(db.node_count(), 2);
assert_eq!(db.edge_count(), 1);
db.close().unwrap();
}
assert!(
!sidecar_wal_path(&path).exists(),
"sidecar WAL should be removed after close"
);
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert_eq!(db.node_count(), 2);
assert_eq!(db.edge_count(), 1);
let session = db.session();
let result = session
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name")
.unwrap();
assert_eq!(extract_strings(result.rows()), vec!["Alix", "Gus"]);
db.close().unwrap();
}
}
#[test]
fn save_as_grafeo_file_from_in_memory() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("exported.grafeo");
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:City {name: 'Amsterdam'})")
.unwrap();
session.execute("INSERT (:City {name: 'Berlin'})").unwrap();
assert_eq!(db.node_count(), 2);
db.save(&path).unwrap();
let db2 = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert_eq!(db2.node_count(), 2);
let session2 = db2.session();
let result = session2
.execute("MATCH (c:City) RETURN c.name ORDER BY c.name")
.unwrap();
assert_eq!(extract_strings(result.rows()), vec!["Amsterdam", "Berlin"]);
db2.close().unwrap();
}
#[test]
fn wal_checkpoint_writes_to_file() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("checkpoint.grafeo");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Vincent'})")
.unwrap();
db.wal_checkpoint().unwrap();
let fm = db.file_manager().expect("should have file manager");
let header = fm.active_header();
assert!(header.iteration > 0, "checkpoint should have been written");
assert_eq!(header.node_count, 1);
assert_eq!(header.edge_count, 0);
db.close().unwrap();
}
#[test]
fn multiple_checkpoints_alternate_headers() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("multi.grafeo");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Jules'})").unwrap();
db.wal_checkpoint().unwrap();
let fm = db.file_manager().unwrap();
assert_eq!(fm.active_header().iteration, 1);
session.execute("INSERT (:Person {name: 'Mia'})").unwrap();
db.wal_checkpoint().unwrap();
assert_eq!(fm.active_header().iteration, 2);
assert_eq!(fm.active_header().node_count, 2);
db.close().unwrap();
let db2 = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert_eq!(db2.node_count(), 2);
db2.close().unwrap();
}
#[test]
fn auto_detect_does_not_use_grafeo_file_for_directory_path() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("test_legacy");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
#[cfg(feature = "grafeo-file")]
assert!(
db.file_manager().is_none(),
"directory path should not use single-file format"
);
let session = db.session();
session.execute("INSERT (:Person {name: 'Butch'})").unwrap();
db.close().unwrap();
assert!(path.is_dir());
}
#[test]
fn info_reports_persistence() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("info.grafeo");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let info = db.info();
assert!(info.is_persistent);
assert!(info.path.is_some());
db.close().unwrap();
}
#[test]
fn checkpoint_merges_incremental_writes() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("merge.grafeo");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.execute("INSERT (:Person {name: 'Gus'})").unwrap();
session
.execute("INSERT (:Person {name: 'Vincent'})")
.unwrap();
db.wal_checkpoint().unwrap();
assert_eq!(db.file_manager().unwrap().active_header().node_count, 3);
session.execute("INSERT (:Person {name: 'Jules'})").unwrap();
session.execute("INSERT (:Person {name: 'Mia'})").unwrap();
session
.execute("MATCH (p:Person {name: 'Alix'}) SET p.age = 31")
.unwrap();
db.wal_checkpoint().unwrap();
assert_eq!(db.file_manager().unwrap().active_header().node_count, 5);
session
.execute("MATCH (p:Person {name: 'Gus'}) DELETE p")
.unwrap();
session
.execute(
"MATCH (a:Person {name: 'Vincent'}), (b:Person {name: 'Jules'}) \
INSERT (a)-[:KNOWS]->(b)",
)
.unwrap();
db.wal_checkpoint().unwrap();
let header = db.file_manager().unwrap().active_header();
assert_eq!(header.node_count, 4);
assert_eq!(header.edge_count, 1);
assert_eq!(header.iteration, 3);
db.close().unwrap();
let db2 = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert_eq!(db2.node_count(), 4);
assert_eq!(db2.edge_count(), 1);
let session2 = db2.session();
let result = session2
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name")
.unwrap();
assert_eq!(
extract_strings(result.rows()),
vec!["Alix", "Jules", "Mia", "Vincent"]
);
let result = session2
.execute("MATCH (p:Person {name: 'Alix'}) RETURN p.age")
.unwrap();
assert_eq!(result.rows()[0][0], Value::Int64(31));
db2.close().unwrap();
}
#[test]
fn writes_after_checkpoint_survive_reopen() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("post_checkpoint.grafeo");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session
.execute("INSERT (:City {name: 'Amsterdam'})")
.unwrap();
db.wal_checkpoint().unwrap();
session.execute("INSERT (:City {name: 'Berlin'})").unwrap();
session.execute("INSERT (:City {name: 'Prague'})").unwrap();
db.close().unwrap();
let db2 = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert_eq!(db2.node_count(), 3);
let session2 = db2.session();
let result = session2
.execute("MATCH (c:City) RETURN c.name ORDER BY c.name")
.unwrap();
assert_eq!(
extract_strings(result.rows()),
vec!["Amsterdam", "Berlin", "Prague"]
);
db2.close().unwrap();
}
#[test]
fn empty_database_roundtrip() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("empty.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert_eq!(db.node_count(), 0);
assert_eq!(db.edge_count(), 0);
db.close().unwrap();
}
}
#[test]
fn multiple_reopen_cycles() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("cycles.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
db.session()
.execute("INSERT (:Person {name: 'Alix'})")
.unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert_eq!(db.node_count(), 1);
db.session()
.execute("INSERT (:Person {name: 'Gus'})")
.unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert_eq!(db.node_count(), 2);
db.session()
.execute("INSERT (:Person {name: 'Vincent'})")
.unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert_eq!(db.node_count(), 3);
let session = db.session();
let result = session
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name")
.unwrap();
assert_eq!(
extract_strings(result.rows()),
vec!["Alix", "Gus", "Vincent"]
);
db.close().unwrap();
}
}
#[test]
fn large_property_values_roundtrip() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("large_props.grafeo");
let big_string = "x".repeat(100_000);
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session
.execute_with_params(
"INSERT (:Doc {content: $text})",
[("text".to_string(), Value::String(big_string.clone().into()))]
.into_iter()
.collect(),
)
.unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
let result = session.execute("MATCH (d:Doc) RETURN d.content").unwrap();
match &result.rows()[0][0] {
Value::String(s) => assert_eq!(s.len(), 100_000),
other => panic!("expected String, got {other:?}"),
}
db.close().unwrap();
}
}
#[test]
fn diverse_property_types_roundtrip() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("types.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session
.execute(
"INSERT (:Thing { \
str_val: 'hello', \
int_val: 42, \
float_val: 3.14, \
bool_val: true, \
list_val: [1, 2, 3] \
})",
)
.unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
let result = session
.execute(
"MATCH (t:Thing) RETURN t.str_val, t.int_val, t.float_val, t.bool_val, t.list_val",
)
.unwrap();
let row = &result.rows()[0];
assert_eq!(row[0], Value::String("hello".into()));
assert_eq!(row[1], Value::Int64(42));
assert!(matches!(row[2], Value::Float64(_)));
assert_eq!(row[3], Value::Bool(true));
assert!(matches!(row[4], Value::List(_)));
db.close().unwrap();
}
}
#[test]
fn open_nonexistent_creates_new() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("new.grafeo");
assert!(!path.exists());
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert!(path.exists());
assert_eq!(db.node_count(), 0);
db.close().unwrap();
}
#[test]
fn file_grows_and_shrinks_with_data() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("size.grafeo");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let fm = db.file_manager().unwrap();
let initial_size = fm.file_size().unwrap();
let session = db.session();
for i in 0..100 {
session
.execute(&format!(
"INSERT (:Node {{idx: {i}, data: '{}'}})",
"x".repeat(1000)
))
.unwrap();
}
db.wal_checkpoint().unwrap();
let large_size = fm.file_size().unwrap();
assert!(large_size > initial_size, "file should grow with data");
session
.execute("MATCH (n:Node) WHERE n.idx > 5 DELETE n")
.unwrap();
db.wal_checkpoint().unwrap();
let small_size = fm.file_size().unwrap();
assert!(
small_size < large_size,
"file should shrink after deleting data: {small_size} >= {large_size}"
);
db.close().unwrap();
}
#[test]
fn sidecar_wal_exists_during_operation() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("wal_lifecycle.grafeo");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let wal = sidecar_wal_path(&path);
assert!(wal.exists(), "sidecar WAL should exist during operation");
assert!(wal.is_dir(), "sidecar WAL should be a directory");
db.close().unwrap();
assert!(!wal.exists(), "sidecar WAL should be removed after close");
}
#[test]
fn checkpoint_idempotent() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("idempotent.grafeo");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
db.wal_checkpoint().unwrap();
let iter1 = db.file_manager().unwrap().active_header().iteration;
db.wal_checkpoint().unwrap();
let iter2 = db.file_manager().unwrap().active_header().iteration;
db.wal_checkpoint().unwrap();
let iter3 = db.file_manager().unwrap().active_header().iteration;
assert_eq!(iter2, iter1 + 1);
assert_eq!(iter3, iter2 + 1);
assert_eq!(db.node_count(), 1);
db.close().unwrap();
}
#[test]
fn concurrent_sessions_before_close() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("sessions.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let s1 = db.session();
let s2 = db.session();
s1.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s2.execute("INSERT (:Person {name: 'Gus'})").unwrap();
s1.execute("INSERT (:Person {name: 'Vincent'})").unwrap();
assert_eq!(db.node_count(), 3);
db.close().unwrap();
}
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert_eq!(db.node_count(), 3);
db.close().unwrap();
}
}
#[test]
fn named_graphs_persist() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("named_graphs.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session.execute("CREATE GRAPH social").unwrap();
session.execute("USE GRAPH social").unwrap();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.execute("USE GRAPH DEFAULT").unwrap();
session.execute("INSERT (:Person {name: 'Gus'})").unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
let result = session.execute("MATCH (p:Person) RETURN p.name").unwrap();
assert_eq!(extract_strings(result.rows()), vec!["Gus"]);
session.execute("USE GRAPH social").unwrap();
let result = session.execute("MATCH (p:Person) RETURN p.name").unwrap();
assert_eq!(extract_strings(result.rows()), vec!["Alix"]);
db.close().unwrap();
}
}
#[test]
fn edges_with_properties_persist() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("edge_props.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.execute("INSERT (:Person {name: 'Gus'})").unwrap();
session
.execute(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
INSERT (a)-[:KNOWS {since: 2020, strength: 0.95}]->(b)",
)
.unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
let result = session
.execute("MATCH ()-[e:KNOWS]->() RETURN e.since, e.strength")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Int64(2020));
assert!(matches!(result.rows()[0][1], Value::Float64(_)));
db.close().unwrap();
}
}
#[test]
fn corrupt_snapshot_detected_on_open() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("corrupt.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
db.close().unwrap();
}
{
use std::io::{Seek, SeekFrom, Write};
let mut file = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
file.seek(SeekFrom::Start(12288)).unwrap();
file.write_all(b"CORRUPTED DATA HERE!!!").unwrap();
}
let result = GrafeoDB::with_config(Config::persistent(&path));
assert!(result.is_err());
let err_msg = result.err().unwrap().to_string();
let signals_corruption = err_msg.contains("checksum")
|| err_msg.contains("section directory")
|| err_msg.contains("failed to parse");
assert!(
signals_corruption,
"expected a corruption-related error, got: {err_msg}"
);
}
#[test]
fn validate_reports_clean_state() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("valid.grafeo");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.execute("INSERT (:Person {name: 'Gus'})").unwrap();
session
.execute(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
INSERT (a)-[:KNOWS]->(b)",
)
.unwrap();
let validation = db.validate();
assert!(validation.errors.is_empty(), "should have no errors");
db.close().unwrap();
}
#[test]
fn wal_status_reflects_single_file() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("wal_status.grafeo");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let status = db.wal_status();
assert!(status.enabled);
db.close().unwrap();
}
#[test]
fn detailed_stats_with_grafeo_file() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("stats.grafeo");
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
for i in 0..10 {
session
.execute(&format!("INSERT (:Node {{idx: {i}}})"))
.unwrap();
}
let stats = db.detailed_stats();
assert_eq!(stats.node_count, 10);
assert_eq!(stats.edge_count, 0);
db.close().unwrap();
}
#[test]
fn second_open_of_same_file_is_rejected() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("locked.grafeo");
let db1 = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db1.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let result = GrafeoDB::open(&path);
assert!(result.is_err(), "second open should fail due to file lock");
db1.close().unwrap();
let db2 = GrafeoDB::open(&path).unwrap();
assert_eq!(db2.node_count(), 1);
db2.close().unwrap();
}
#[test]
fn lock_released_on_drop() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("drop_lock.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
db.session().execute("INSERT (:X {v: 1})").unwrap();
}
let db2 = GrafeoDB::open(&path).unwrap();
db2.close().unwrap();
}
#[test]
fn node_type_definitions_persist() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("schema.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session
.execute("CREATE NODE TYPE Person (name STRING NOT NULL, age INT64)")
.unwrap();
session
.execute("CREATE NODE TYPE Company (name STRING NOT NULL)")
.unwrap();
session
.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::open(&path).unwrap();
let session = db.session();
let result = session.execute("MATCH (p:Person) RETURN p.name").unwrap();
assert_eq!(extract_strings(result.rows()), vec!["Alix"]);
let result = session.execute("SHOW NODE TYPES").unwrap();
let type_names = extract_strings(result.rows());
assert!(
type_names.contains(&"Person".to_string()),
"Person type missing: {type_names:?}"
);
assert!(
type_names.contains(&"Company".to_string()),
"Company type missing: {type_names:?}"
);
db.close().unwrap();
}
}
#[test]
fn edge_type_definitions_persist() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("edge_types.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session
.execute("CREATE EDGE TYPE KNOWS (since INT64)")
.unwrap();
session
.execute("CREATE EDGE TYPE WORKS_AT (role STRING)")
.unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::open(&path).unwrap();
let session = db.session();
let result = session.execute("SHOW EDGE TYPES").unwrap();
let type_names = extract_strings(result.rows());
assert!(
type_names.contains(&"KNOWS".to_string()),
"KNOWS type missing: {type_names:?}"
);
assert!(
type_names.contains(&"WORKS_AT".to_string()),
"WORKS_AT type missing: {type_names:?}"
);
db.close().unwrap();
}
}
#[test]
fn graph_type_definitions_persist() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("graph_types.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session
.execute("CREATE NODE TYPE Person (name STRING)")
.unwrap();
session
.execute("CREATE EDGE TYPE KNOWS (since INT64)")
.unwrap();
session
.execute(
"CREATE GRAPH TYPE SocialGraph (\
NODE TYPE Person (name STRING),\
EDGE TYPE KNOWS (since INT64)\
)",
)
.unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::open(&path).unwrap();
let session = db.session();
let result = session.execute("SHOW GRAPH TYPES").unwrap();
let type_names = extract_strings(result.rows());
assert!(
type_names.contains(&"SocialGraph".to_string()),
"SocialGraph type missing: {type_names:?}"
);
db.close().unwrap();
}
}
#[test]
fn schema_survives_export_import_roundtrip() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("CREATE NODE TYPE Person (name STRING NOT NULL, age INT64)")
.unwrap();
session
.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
let snapshot = db.export_snapshot().unwrap();
let db2 = GrafeoDB::import_snapshot(&snapshot).unwrap();
let session2 = db2.session();
let result = session2.execute("MATCH (p:Person) RETURN p.name").unwrap();
assert_eq!(extract_strings(result.rows()), vec!["Alix"]);
let result = session2.execute("SHOW NODE TYPES").unwrap();
let type_names = extract_strings(result.rows());
assert!(
type_names.contains(&"Person".to_string()),
"Person type missing after import: {type_names:?}"
);
}
#[test]
#[cfg(feature = "algos")]
fn stored_procedures_persist() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("procedures.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session
.execute(
"CREATE PROCEDURE get_people() RETURNS (name STRING) \
AS { MATCH (p:Person) RETURN p.name AS name }",
)
.unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::open(&path).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let result = session.execute("CALL get_people()").unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(extract_strings(result.rows()), vec!["Alix"]);
db.close().unwrap();
}
}
#[test]
fn schema_with_data_across_multiple_cycles() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("schema_cycles.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session
.execute("CREATE NODE TYPE Person (name STRING NOT NULL)")
.unwrap();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::open(&path).unwrap();
let session = db.session();
let result = session.execute("SHOW NODE TYPES").unwrap();
assert!(extract_strings(result.rows()).contains(&"Person".to_string()));
session
.execute("CREATE NODE TYPE City (name STRING NOT NULL)")
.unwrap();
session
.execute("INSERT (:City {name: 'Amsterdam'})")
.unwrap();
db.close().unwrap();
}
{
let db = GrafeoDB::open(&path).unwrap();
let session = db.session();
let result = session.execute("SHOW NODE TYPES").unwrap();
let types = extract_strings(result.rows());
assert!(types.contains(&"Person".to_string()), "Person missing");
assert!(types.contains(&"City".to_string()), "City missing");
assert_eq!(db.node_count(), 2);
db.close().unwrap();
}
}
#[test]
fn wal_disabled_single_file_persists_on_close() {
use grafeo_engine::config::StorageFormat;
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("no_wal.grafeo");
{
let config = Config {
wal_enabled: false,
..Config::persistent(&path).with_storage_format(StorageFormat::SingleFile)
};
let db = GrafeoDB::with_config(config).unwrap();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
session
.execute("INSERT (:City {name: 'Amsterdam'})")
.unwrap();
assert_eq!(db.node_count(), 2);
assert!(
!sidecar_wal_path(&path).exists(),
"no sidecar WAL should be created when wal_enabled: false"
);
db.close().unwrap();
}
assert!(
!sidecar_wal_path(&path).exists(),
"sidecar WAL should not exist after close with wal_enabled: false"
);
assert!(path.exists() && path.is_file());
{
let config = Config {
wal_enabled: false,
..Config::persistent(&path).with_storage_format(StorageFormat::SingleFile)
};
let db = GrafeoDB::with_config(config).unwrap();
assert_eq!(
db.node_count(),
2,
"data must survive close-reopen with WAL disabled"
);
let session = db.session();
let result = session.execute("MATCH (p:Person) RETURN p.name").unwrap();
assert_eq!(extract_strings(result.rows()), vec!["Alix"]);
db.close().unwrap();
}
}
#[test]
fn drop_persists_data_and_removes_sidecar_wal() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("drop_implicit_close.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Vincent'})")
.unwrap();
session.execute("INSERT (:Person {name: 'Jules'})").unwrap();
drop(db);
}
assert!(
!sidecar_wal_path(&path).exists(),
"sidecar WAL should be removed after implicit close via drop"
);
assert!(path.exists() && path.is_file());
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
assert_eq!(
db.node_count(),
2,
"both nodes must survive implicit close via drop"
);
let result = db
.session()
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name")
.unwrap();
assert_eq!(extract_strings(result.rows()), vec!["Jules", "Vincent"]);
db.close().unwrap();
}
}
#[test]
fn two_concurrent_read_only_opens() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("shared_ro.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
db.session()
.execute("INSERT (:City {name: 'Amsterdam'})")
.unwrap();
db.close().unwrap();
}
let ro1 = GrafeoDB::open_read_only(&path).unwrap();
let ro2 = GrafeoDB::open_read_only(&path).unwrap();
assert_eq!(ro1.node_count(), 1);
assert_eq!(ro2.node_count(), 1);
let r1 = ro1
.session()
.execute("MATCH (c:City) RETURN c.name")
.unwrap();
let r2 = ro2
.session()
.execute("MATCH (c:City) RETURN c.name")
.unwrap();
assert_eq!(extract_strings(r1.rows()), vec!["Amsterdam"]);
assert_eq!(extract_strings(r2.rows()), vec!["Amsterdam"]);
ro1.close().unwrap();
ro2.close().unwrap();
}
#[test]
fn read_only_blocked_while_writer_holds_lock() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("writer_lock.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
db.session()
.execute("INSERT (:Person {name: 'Mia'})")
.unwrap();
db.close().unwrap();
}
let writer = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let result = GrafeoDB::open_read_only(&path);
assert!(
result.is_err(),
"open_read_only must fail while writer holds exclusive lock"
);
writer.close().unwrap();
let ro = GrafeoDB::open_read_only(&path).unwrap();
assert_eq!(ro.node_count(), 1);
ro.close().unwrap();
}
#[test]
fn wal_data_after_checkpoint_survives_drop() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("wal_after_checkpoint.grafeo");
{
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Butch'})").unwrap();
db.wal_checkpoint().unwrap();
let fm = db.file_manager().unwrap();
assert_eq!(
fm.active_header().node_count,
1,
"checkpoint must capture first node"
);
session
.execute("INSERT (:Person {name: 'Shosanna'})")
.unwrap();
drop(db);
}
{
let db = GrafeoDB::open(&path).unwrap();
assert_eq!(db.node_count(), 2);
let result = db
.session()
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name")
.unwrap();
assert_eq!(extract_strings(result.rows()), vec!["Butch", "Shosanna"]);
db.close().unwrap();
}
}
#[cfg(all(feature = "wal", feature = "lpg"))]
#[test]
fn wal_checkpoint_on_read_only_is_no_op() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ro_checkpoint.grafeo");
{
let db = GrafeoDB::open(&path).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
db.close().unwrap();
}
let db = GrafeoDB::open_read_only(&path).unwrap();
db.wal_checkpoint()
.expect("wal_checkpoint on read-only should be a no-op");
assert_eq!(db.node_count(), 1);
}
#[cfg(all(feature = "wal", feature = "lpg"))]
#[test]
fn save_from_read_only_database() {
let dir = tempfile::tempdir().unwrap();
let src_path = dir.path().join("ro_save_src.grafeo");
let dest_path = dir.path().join("ro_save_dest.grafeo");
{
let db = GrafeoDB::open(&src_path).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.execute("INSERT (:Person {name: 'Gus'})").unwrap();
db.close().unwrap();
}
let db = GrafeoDB::open_read_only(&src_path).unwrap();
db.save(&dest_path)
.expect("save from read-only should succeed");
let restored = GrafeoDB::open(&dest_path).unwrap();
assert_eq!(restored.node_count(), 2);
restored.close().unwrap();
}
#[cfg(all(feature = "compact-store", feature = "lpg"))]
#[test]
fn deleted_base_nodes_stay_deleted_across_reopen() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("layered_delete_persist.grafeo");
{
let mut db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.execute("INSERT (:Person {name: 'Gus'})").unwrap();
session
.execute("INSERT (:Person {name: 'Vincent'})")
.unwrap();
drop(session);
db.compact().expect("compact should succeed");
let session = db.session();
session
.execute("MATCH (p:Person {name: 'Gus'}) DELETE p")
.unwrap();
drop(session);
let session = db.session();
let result = session
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name")
.unwrap();
assert_eq!(extract_strings(result.rows()), vec!["Alix", "Vincent"]);
drop(session);
db.close().unwrap();
}
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
let result = session
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name")
.unwrap();
assert_eq!(
extract_strings(result.rows()),
vec!["Alix", "Vincent"],
"previously-deleted base node must stay deleted across reopen"
);
drop(session);
db.close().unwrap();
}
#[cfg(all(feature = "compact-store", feature = "lpg"))]
#[test]
fn deleted_base_edges_stay_deleted_across_reopen() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("layered_edge_delete_persist.grafeo");
{
let mut db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.execute("INSERT (:Person {name: 'Gus'})").unwrap();
session
.execute(
"MATCH (a:Person {name: 'Alix'}), (b:Person {name: 'Gus'}) \
INSERT (a)-[:KNOWS]->(b)",
)
.unwrap();
drop(session);
db.compact().expect("compact should succeed");
let session = db.session();
session
.execute("MATCH (:Person)-[r:KNOWS]->(:Person) DELETE r")
.unwrap();
let result = session
.execute("MATCH (:Person)-[r:KNOWS]->(:Person) RETURN count(r)")
.unwrap();
assert_eq!(result.rows()[0][0], Value::Int64(0));
drop(session);
db.close().unwrap();
}
let db = GrafeoDB::with_config(Config::persistent(&path)).unwrap();
let session = db.session();
let result = session
.execute("MATCH (:Person)-[r:KNOWS]->(:Person) RETURN count(r)")
.unwrap();
assert_eq!(
result.rows()[0][0],
Value::Int64(0),
"previously-deleted base edge must stay deleted across reopen"
);
drop(session);
db.close().unwrap();
}