use grafeo_common::types::{EdgeId, EpochId, NodeId, Value};
use grafeo_engine::GrafeoDB;
#[derive(serde::Serialize, serde::Deserialize)]
struct TestSnapshot {
version: u8,
nodes: Vec<TestNode>,
edges: Vec<TestEdge>,
named_graphs: Vec<()>,
rdf_triples: Vec<()>,
rdf_named_graphs: Vec<()>,
schema: TestSnapshotSchema,
indexes: TestSnapshotIndexes,
epoch: u64,
}
#[derive(serde::Serialize, serde::Deserialize, Default)]
struct TestSnapshotSchema {
node_types: Vec<()>,
edge_types: Vec<()>,
graph_types: Vec<()>,
procedures: Vec<()>,
schemas: Vec<()>,
graph_type_bindings: Vec<()>,
}
#[derive(serde::Serialize, serde::Deserialize, Default)]
struct TestSnapshotIndexes {
property_indexes: Vec<()>,
vector_indexes: Vec<()>,
text_indexes: Vec<()>,
}
impl TestSnapshot {
fn new(version: u8, nodes: Vec<TestNode>, edges: Vec<TestEdge>) -> Self {
Self {
version,
nodes,
edges,
named_graphs: vec![],
rdf_triples: vec![],
rdf_named_graphs: vec![],
schema: TestSnapshotSchema::default(),
indexes: TestSnapshotIndexes::default(),
epoch: 0,
}
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct TestNode {
id: NodeId,
labels: Vec<String>,
properties: Vec<(String, Vec<(EpochId, Value)>)>,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct TestEdge {
id: EdgeId,
src: NodeId,
dst: NodeId,
edge_type: String,
properties: Vec<(String, Vec<(EpochId, Value)>)>,
}
fn encode_snapshot(snap: &TestSnapshot) -> Vec<u8> {
bincode::serde::encode_to_vec(snap, bincode::config::standard()).unwrap()
}
#[test]
fn export_import_empty_database() {
let db = GrafeoDB::new_in_memory();
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
assert_eq!(restored.node_count(), 0);
assert_eq!(restored.edge_count(), 0);
}
#[test]
fn export_import_preserves_nodes() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
session
.execute("INSERT (:Person {name: 'Gus', age: 25})")
.unwrap();
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
assert_eq!(restored.node_count(), 2);
let session2 = restored.session();
let result = session2
.execute("MATCH (p:Person) RETURN p.name ORDER BY p.name")
.unwrap();
assert_eq!(result.rows().len(), 2);
}
#[test]
fn export_import_preserves_edges() {
let db = GrafeoDB::new_in_memory();
let alix = db.create_node(&["Person"]);
db.set_node_property(alix, "name", "Alix".into());
let gus = db.create_node(&["Person"]);
db.set_node_property(gus, "name", "Gus".into());
db.create_edge(alix, gus, "KNOWS");
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
assert_eq!(restored.node_count(), 2);
assert_eq!(restored.edge_count(), 1);
let session2 = restored.session();
let result = session2
.execute("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name")
.unwrap();
assert_eq!(result.rows().len(), 1);
}
#[test]
fn export_import_preserves_properties() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session
.execute("INSERT (:Item {name: 'Widget', price: 9.99, active: true})")
.unwrap();
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
let session2 = restored.session();
let result = session2
.execute("MATCH (i:Item) RETURN i.name, i.price, i.active")
.unwrap();
assert_eq!(result.rows().len(), 1);
}
#[test]
fn import_rejects_invalid_data() {
let result = GrafeoDB::import_snapshot(b"not a valid snapshot");
assert!(result.is_err());
}
#[test]
fn snapshot_round_trip_schema() {
let db = GrafeoDB::new_in_memory();
let alix = db.create_node(&["Person"]);
db.set_node_property(alix, "name", "Alix".into());
let gus = db.create_node(&["Person"]);
db.set_node_property(gus, "name", "Gus".into());
db.create_edge(alix, gus, "KNOWS");
let schema_before = db.schema();
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
let schema_after = restored.schema();
let fmt_before = format!("{schema_before:?}");
let fmt_after = format!("{schema_after:?}");
assert_eq!(fmt_before, fmt_after);
}
#[test]
fn export_import_preserves_edge_properties() {
let db = GrafeoDB::new_in_memory();
let a = db.create_node(&["Person"]);
let b = db.create_node(&["Person"]);
let edge = db.create_edge(a, b, "KNOWS");
db.set_edge_property(edge, "since", Value::Int64(2020));
db.set_edge_property(edge, "strength", Value::Float64(0.95));
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
let session = restored.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_eq!(result.rows()[0][1], Value::Float64(0.95));
}
#[test]
fn export_import_preserves_multiple_labels() {
let db = GrafeoDB::new_in_memory();
db.create_node(&["Person", "Employee"]);
db.create_node(&["Person", "Manager"]);
db.create_node(&["Animal"]);
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
assert_eq!(restored.node_count(), 3);
let session = restored.session();
let persons = session.execute("MATCH (p:Person) RETURN p").unwrap();
assert_eq!(persons.rows().len(), 2);
let employees = session.execute("MATCH (e:Employee) RETURN e").unwrap();
assert_eq!(employees.rows().len(), 1);
let managers = session.execute("MATCH (m:Manager) RETURN m").unwrap();
assert_eq!(managers.rows().len(), 1);
let animals = session.execute("MATCH (a:Animal) RETURN a").unwrap();
assert_eq!(animals.rows().len(), 1);
}
#[test]
fn export_import_preserves_temporal_values() {
use grafeo_common::types::{Date, Duration, Time, Timestamp, ZonedDatetime};
let db = GrafeoDB::new_in_memory();
let id = db.create_node(&["Temporal"]);
let date = Date::from_ymd(2025, 6, 15).unwrap();
let time = Time::from_hms(14, 30, 0).unwrap();
let timestamp = Timestamp::from_secs(1_700_000_000);
let duration = Duration::new(1, 15, 3_600_000_000_000); let zoned = ZonedDatetime::from_timestamp_offset(Timestamp::from_secs(1_700_000_000), 3600);
db.set_node_property(id, "date_val", Value::Date(date));
db.set_node_property(id, "time_val", Value::Time(time));
db.set_node_property(id, "ts_val", Value::Timestamp(timestamp));
db.set_node_property(id, "dur_val", Value::Duration(duration));
db.set_node_property(id, "zdt_val", Value::ZonedDatetime(zoned));
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
let session = restored.session();
let result = session
.execute("MATCH (t:Temporal) RETURN t.date_val, t.time_val, t.ts_val, t.dur_val, t.zdt_val")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::Date(date));
assert_eq!(result.rows()[0][1], Value::Time(time));
assert_eq!(result.rows()[0][2], Value::Timestamp(timestamp));
assert_eq!(result.rows()[0][3], Value::Duration(duration));
assert_eq!(result.rows()[0][4], Value::ZonedDatetime(zoned));
}
#[test]
fn export_import_preserves_all_value_types() {
let db = GrafeoDB::new_in_memory();
let id = db.create_node(&["Test"]);
db.set_node_property(id, "str_val", Value::String("hello".into()));
db.set_node_property(id, "int_val", Value::Int64(42));
db.set_node_property(id, "float_val", Value::Float64(9.81));
db.set_node_property(id, "bool_val", Value::Bool(true));
db.set_node_property(id, "null_val", Value::Null);
db.set_node_property(
id,
"bytes_val",
Value::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF].into()),
);
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
let session = restored.session();
let result = session
.execute("MATCH (t:Test) RETURN t.str_val, t.int_val, t.float_val, t.bool_val, t.null_val, t.bytes_val")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("hello".into()));
assert_eq!(result.rows()[0][1], Value::Int64(42));
assert_eq!(result.rows()[0][2], Value::Float64(9.81));
assert_eq!(result.rows()[0][3], Value::Bool(true));
assert_eq!(result.rows()[0][4], Value::Null);
assert_eq!(
result.rows()[0][5],
Value::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF].into())
);
}
#[test]
fn export_import_preserves_collection_values() {
let db = GrafeoDB::new_in_memory();
let id = db.create_node(&["Test"]);
db.set_node_property(
id,
"tags",
Value::List(vec![Value::String("a".into()), Value::String("b".into())].into()),
);
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
let session = restored.session();
let result = session.execute("MATCH (t:Test) RETURN t.tags").unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(
result.rows()[0][0],
Value::List(vec![Value::String("a".into()), Value::String("b".into()),].into())
);
}
#[test]
fn export_import_preserves_multiple_edge_types() {
let db = GrafeoDB::new_in_memory();
let a = db.create_node(&["Person"]);
let b = db.create_node(&["Person"]);
let c = db.create_node(&["Company"]);
db.create_edge(a, b, "KNOWS");
db.create_edge(a, c, "WORKS_AT");
db.create_edge(b, c, "WORKS_AT");
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
assert_eq!(restored.node_count(), 3);
assert_eq!(restored.edge_count(), 3);
let session = restored.session();
let knows = session.execute("MATCH ()-[e:KNOWS]->() RETURN e").unwrap();
assert_eq!(knows.rows().len(), 1);
let works = session
.execute("MATCH ()-[e:WORKS_AT]->() RETURN e")
.unwrap();
assert_eq!(works.rows().len(), 2);
}
#[test]
fn export_import_preserves_empty_property_nodes() {
let db = GrafeoDB::new_in_memory();
db.create_node(&["Empty"]);
db.create_node(&["Empty"]);
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
assert_eq!(restored.node_count(), 2);
let session = restored.session();
let result = session.execute("MATCH (e:Empty) RETURN e").unwrap();
assert_eq!(result.rows().len(), 2);
}
#[test]
fn export_import_moderate_dataset() {
let db = GrafeoDB::new_in_memory();
let mut ids = Vec::new();
for i in 0..100 {
let id = db.create_node(&["Item"]);
db.set_node_property(id, "index", Value::Int64(i));
db.set_node_property(id, "name", Value::String(format!("item_{i}").into()));
ids.push(id);
}
for i in 0..50 {
db.create_edge(ids[i], ids[i + 50], "LINKS_TO");
}
let bytes = db.export_snapshot().unwrap();
assert!(!bytes.is_empty());
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
assert_eq!(restored.node_count(), 100);
assert_eq!(restored.edge_count(), 50);
let session = restored.session();
let result = session
.execute("MATCH (i:Item) WHERE i.index = 42 RETURN i.name")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("item_42".into()));
}
#[test]
fn import_rejects_empty_bytes() {
let result = GrafeoDB::import_snapshot(&[]);
assert!(result.is_err());
}
#[test]
fn import_rejects_unsupported_version() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let mut bytes = db.export_snapshot().unwrap();
bytes[0] = 99;
let result = GrafeoDB::import_snapshot(&bytes);
match result {
Ok(_) => panic!("Expected error for tampered snapshot"),
Err(e) => {
let err_msg = e.to_string();
assert!(
err_msg.contains("snapshot")
|| err_msg.contains("unsupported")
|| err_msg.contains("import"),
"Expected snapshot error, got: {err_msg}"
);
}
}
}
#[test]
fn double_export_is_deterministic() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let bytes1 = db.export_snapshot().unwrap();
let bytes2 = db.export_snapshot().unwrap();
assert_eq!(bytes1, bytes2);
}
#[test]
fn import_rejects_dangling_edge_source() {
let db = GrafeoDB::new_in_memory();
let a = db.create_node(&["Person"]);
let b = db.create_node(&["Person"]);
db.create_edge(a, b, "KNOWS");
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
assert_eq!(restored.edge_count(), 1);
let db2 = GrafeoDB::new_in_memory();
let n = db2.create_node(&["Person"]);
db2.store()
.create_edge_with_id(
grafeo_common::types::EdgeId::new(0),
grafeo_common::types::NodeId::new(999), n,
"KNOWS",
)
.unwrap();
let bytes = db2.export_snapshot().unwrap();
let result = GrafeoDB::import_snapshot(&bytes);
match result {
Ok(_) => panic!("Expected error for dangling source node"),
Err(e) => {
let err = e.to_string();
assert!(
err.contains("non-existent source node"),
"Expected dangling source error, got: {err}"
);
}
}
}
#[test]
fn import_rejects_dangling_edge_destination() {
let db = GrafeoDB::new_in_memory();
let n = db.create_node(&["Person"]);
db.store()
.create_edge_with_id(
grafeo_common::types::EdgeId::new(0),
n,
grafeo_common::types::NodeId::new(999), "KNOWS",
)
.unwrap();
let bytes = db.export_snapshot().unwrap();
let result = GrafeoDB::import_snapshot(&bytes);
match result {
Ok(_) => panic!("Expected error for dangling destination node"),
Err(e) => {
let err = e.to_string();
assert!(
err.contains("non-existent destination node"),
"Expected dangling destination error, got: {err}"
);
}
}
}
#[test]
fn import_rejects_duplicate_node_ids() {
let snap = TestSnapshot::new(
4,
vec![
TestNode {
id: NodeId::new(0),
labels: vec!["A".into()],
properties: vec![],
},
TestNode {
id: NodeId::new(0), labels: vec!["B".into()],
properties: vec![],
},
],
vec![],
);
let bytes = encode_snapshot(&snap);
let result = GrafeoDB::import_snapshot(&bytes);
match result {
Ok(_) => panic!("Expected error for duplicate node ID"),
Err(e) => {
let err = e.to_string();
assert!(
err.contains("duplicate node ID"),
"Expected duplicate node error, got: {err}"
);
}
}
}
#[test]
fn import_rejects_duplicate_edge_ids() {
let snap = TestSnapshot::new(
4,
vec![
TestNode {
id: NodeId::new(0),
labels: vec![],
properties: vec![],
},
TestNode {
id: NodeId::new(1),
labels: vec![],
properties: vec![],
},
],
vec![
TestEdge {
id: EdgeId::new(0),
src: NodeId::new(0),
dst: NodeId::new(1),
edge_type: "REL".into(),
properties: vec![],
},
TestEdge {
id: EdgeId::new(0), src: NodeId::new(0),
dst: NodeId::new(1),
edge_type: "REL".into(),
properties: vec![],
},
],
);
let bytes = encode_snapshot(&snap);
let result = GrafeoDB::import_snapshot(&bytes);
match result {
Ok(_) => panic!("Expected error for duplicate edge ID"),
Err(e) => {
let err = e.to_string();
assert!(
err.contains("duplicate edge ID"),
"Expected duplicate edge error, got: {err}"
);
}
}
}
#[test]
fn export_import_preserves_named_graphs() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.execute("CREATE GRAPH analytics").unwrap();
session.execute("USE GRAPH analytics").unwrap();
session
.execute("INSERT (:KPI {name: 'pageviews', count: 42})")
.unwrap();
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
assert_eq!(restored.node_count(), 1);
let session2 = restored.session();
let result = session2.execute("MATCH (p:Person) RETURN p.name").unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
session2.execute("USE GRAPH analytics").unwrap();
let result = session2
.execute("MATCH (m:KPI) RETURN m.name, m.count")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("pageviews".into()));
assert_eq!(result.rows()[0][1], Value::Int64(42));
}
#[test]
fn export_import_preserves_multiple_named_graphs() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("CREATE GRAPH alpha").unwrap();
session.execute("USE GRAPH alpha").unwrap();
session.execute("INSERT (:Item {name: 'Widget'})").unwrap();
session.execute("USE GRAPH default").unwrap();
session.execute("CREATE GRAPH beta").unwrap();
session.execute("USE GRAPH beta").unwrap();
session
.execute("INSERT (:City {name: 'Amsterdam'})")
.unwrap();
session.execute("INSERT (:City {name: 'Berlin'})").unwrap();
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
let session2 = restored.session();
session2.execute("USE GRAPH alpha").unwrap();
let result = session2.execute("MATCH (i:Item) RETURN i.name").unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Widget".into()));
session2.execute("USE GRAPH beta").unwrap();
let result = session2
.execute("MATCH (c:City) RETURN c.name ORDER BY c.name")
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][0], Value::String("Amsterdam".into()));
assert_eq!(result.rows()[1][0], Value::String("Berlin".into()));
}
#[test]
fn restore_snapshot_includes_named_graphs() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.execute("CREATE GRAPH metrics").unwrap();
session.execute("USE GRAPH metrics").unwrap();
session.execute("INSERT (:KPI {name: 'clicks'})").unwrap();
let snapshot = db.export_snapshot().unwrap();
session.execute("USE GRAPH default").unwrap();
session.execute("INSERT (:Person {name: 'Gus'})").unwrap();
db.restore_snapshot(&snapshot).unwrap();
assert_eq!(db.node_count(), 1, "default graph restored to 1 node");
let session2 = db.session();
session2.execute("USE GRAPH metrics").unwrap();
let result = session2.execute("MATCH (m:KPI) RETURN m.name").unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("clicks".into()));
}
#[test]
fn import_v1_snapshot_is_rejected() {
let snap = TestSnapshot::new(
1,
vec![TestNode {
id: NodeId::new(0),
labels: vec!["Person".into()],
properties: vec![(
"name".into(),
vec![(EpochId::new(0), Value::String("Alix".into()))],
)],
}],
vec![],
);
let bytes = encode_snapshot(&snap);
let result = GrafeoDB::import_snapshot(&bytes);
assert!(result.is_err(), "V1 snapshots should be rejected");
let err = result.err().unwrap().to_string();
assert!(
err.contains("unsupported snapshot version"),
"Expected version error, got: {err}"
);
}
#[test]
fn to_memory_copies_named_graphs() {
let db = GrafeoDB::new_in_memory();
let session = db.session();
session.execute("INSERT (:Person {name: 'Alix'})").unwrap();
session.execute("CREATE GRAPH backup").unwrap();
session.execute("USE GRAPH backup").unwrap();
session
.execute("INSERT (:Archive {date: '2025-01-01'})")
.unwrap();
let copy = db.to_memory().unwrap();
assert_eq!(copy.node_count(), 1);
let session2 = copy.session();
session2.execute("USE GRAPH backup").unwrap();
let result = session2.execute("MATCH (a:Archive) RETURN a.date").unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("2025-01-01".into()));
session
.execute("INSERT (:Archive {date: '2025-02-01'})")
.unwrap();
let result2 = session2.execute("MATCH (a:Archive) RETURN a.date").unwrap();
assert_eq!(result2.rows().len(), 1, "copy should still have 1 node");
}
#[cfg(all(feature = "sparql", feature = "triple-store"))]
mod rdf_snapshots {
use grafeo_common::types::Value;
use grafeo_engine::{Config, GrafeoDB, GraphModel};
fn rdf_db() -> GrafeoDB {
GrafeoDB::with_config(Config::in_memory().with_graph_model(GraphModel::Rdf)).unwrap()
}
#[test]
fn export_import_preserves_rdf_triples() {
let db = rdf_db();
let session = db.session();
session
.execute_sparql(
r#"INSERT DATA {
<http://ex.org/alix> <http://ex.org/name> "Alix" .
<http://ex.org/gus> <http://ex.org/name> "Gus" .
}"#,
)
.unwrap();
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
let session2 = restored.session();
let result = session2
.execute_sparql("SELECT ?name WHERE { ?s <http://ex.org/name> ?name } ORDER BY ?name")
.unwrap();
assert_eq!(result.rows().len(), 2);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
assert_eq!(result.rows()[1][0], Value::String("Gus".into()));
}
#[test]
fn export_import_preserves_rdf_named_graphs() {
let db = rdf_db();
let session = db.session();
session
.execute_sparql(
r#"INSERT DATA {
<http://ex.org/alix> <http://ex.org/name> "Alix" .
GRAPH <http://ex.org/g1> {
<http://ex.org/gus> <http://ex.org/name> "Gus" .
}
}"#,
)
.unwrap();
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
let session2 = restored.session();
let result = session2
.execute_sparql("SELECT ?name WHERE { ?s <http://ex.org/name> ?name }")
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
let result = session2
.execute_sparql(
r#"SELECT ?name WHERE {
GRAPH <http://ex.org/g1> { ?s <http://ex.org/name> ?name }
}"#,
)
.unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(result.rows()[0][0], Value::String("Gus".into()));
}
#[test]
fn restore_snapshot_includes_rdf_data() {
let db = rdf_db();
let session = db.session();
session
.execute_sparql(
r#"INSERT DATA {
<http://ex.org/alix> <http://ex.org/name> "Alix" .
}"#,
)
.unwrap();
let snapshot = db.export_snapshot().unwrap();
session
.execute_sparql(
r#"INSERT DATA {
<http://ex.org/gus> <http://ex.org/name> "Gus" .
}"#,
)
.unwrap();
db.restore_snapshot(&snapshot).unwrap();
let session2 = db.session();
let result = session2
.execute_sparql("SELECT ?name WHERE { ?s <http://ex.org/name> ?name }")
.unwrap();
assert_eq!(result.rows().len(), 1, "restore should revert to snapshot");
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
}
#[test]
fn to_memory_copies_rdf_data() {
let db = rdf_db();
let session = db.session();
session
.execute_sparql(
r#"INSERT DATA {
<http://ex.org/alix> <http://ex.org/name> "Alix" .
GRAPH <http://ex.org/g1> {
<http://ex.org/gus> <http://ex.org/name> "Gus" .
}
}"#,
)
.unwrap();
let copy = db.to_memory().unwrap();
let session2 = copy.session();
let result = session2
.execute_sparql("SELECT ?name WHERE { ?s <http://ex.org/name> ?name }")
.unwrap();
assert_eq!(result.rows().len(), 1, "default RDF graph copied");
assert_eq!(result.rows()[0][0], Value::String("Alix".into()));
let result = session2
.execute_sparql(
r#"SELECT ?name WHERE {
GRAPH <http://ex.org/g1> { ?s <http://ex.org/name> ?name }
}"#,
)
.unwrap();
assert_eq!(result.rows().len(), 1, "named RDF graph copied");
assert_eq!(result.rows()[0][0], Value::String("Gus".into()));
session
.execute_sparql(
r#"INSERT DATA {
<http://ex.org/mia> <http://ex.org/name> "Mia" .
}"#,
)
.unwrap();
let result = session2
.execute_sparql("SELECT ?s WHERE { ?s ?p ?o }")
.unwrap();
assert_eq!(result.rows().len(), 1, "copy should be independent");
}
#[test]
fn export_import_preserves_typed_rdf_literals() {
let db = rdf_db();
let session = db.session();
session
.execute_sparql(
r#"INSERT DATA {
<http://ex.org/alix> <http://ex.org/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> .
<http://ex.org/alix> <http://ex.org/greeting> "Bonjour"@fr .
}"#,
)
.unwrap();
let bytes = db.export_snapshot().unwrap();
let restored = GrafeoDB::import_snapshot(&bytes).unwrap();
let session2 = restored.session();
let result = session2
.execute_sparql("SELECT ?o WHERE { <http://ex.org/alix> ?p ?o } ORDER BY ?o")
.unwrap();
assert_eq!(
result.rows().len(),
2,
"typed and lang literals should survive"
);
}
}
#[test]
fn import_unknown_snapshot_version_returns_clear_error() {
let snap = TestSnapshot::new(99, vec![], vec![]);
let bytes = encode_snapshot(&snap);
let result = GrafeoDB::import_snapshot(&bytes);
match result {
Err(e) => {
let err = e.to_string();
assert!(
err.contains("version") || err.contains("unsupported") || err.contains("99"),
"error should mention version issue, got: {err}"
);
}
Ok(_) => panic!("importing an unknown snapshot version should error"),
}
}
#[test]
fn import_truncated_snapshot_returns_error() {
let db = GrafeoDB::new_in_memory();
db.create_node(&["Test"]);
let bytes = db.export_snapshot().unwrap();
let truncated = &bytes[..bytes.len() / 2];
let result = GrafeoDB::import_snapshot(truncated);
assert!(
result.is_err(),
"importing a truncated snapshot should error"
);
}
#[test]
fn import_empty_bytes_returns_error() {
let result = GrafeoDB::import_snapshot(&[]);
assert!(result.is_err(), "importing empty bytes should error");
}