use super::*;
use crate::graph::Direction;
use crate::graph::lpg::property::CompareOp;
use grafeo_common::types::TransactionId;
#[test]
fn test_create_node() {
let store = LpgStore::new().unwrap();
let id = store.create_node(&["Person"]);
assert!(id.is_valid());
let node = store.get_node(id).unwrap();
assert!(node.has_label("Person"));
assert!(!node.has_label("Animal"));
}
#[test]
fn test_create_node_with_props() {
let store = LpgStore::new().unwrap();
let id = store.create_node_with_props(
&["Person"],
[("name", Value::from("Alix")), ("age", Value::from(30i64))],
);
let node = store.get_node(id).unwrap();
assert_eq!(
node.get_property("name").and_then(|v| v.as_str()),
Some("Alix")
);
assert_eq!(
node.get_property("age").and_then(|v| v.as_int64()),
Some(30)
);
}
#[test]
fn test_delete_node() {
let store = LpgStore::new().unwrap();
let id = store.create_node(&["Person"]);
assert_eq!(store.node_count(), 1);
assert!(store.delete_node(id));
assert_eq!(store.node_count(), 0);
assert!(store.get_node(id).is_none());
assert!(!store.delete_node(id));
}
#[test]
fn test_create_edge() {
let store = LpgStore::new().unwrap();
let alix = store.create_node(&["Person"]);
let gus = store.create_node(&["Person"]);
let edge_id = store.create_edge(alix, gus, "KNOWS");
assert!(edge_id.is_valid());
let edge = store.get_edge(edge_id).unwrap();
assert_eq!(edge.src, alix);
assert_eq!(edge.dst, gus);
assert_eq!(edge.edge_type.as_str(), "KNOWS");
}
#[test]
fn test_neighbors() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let c = store.create_node(&["Person"]);
store.create_edge(a, b, "KNOWS");
store.create_edge(a, c, "KNOWS");
let outgoing: Vec<_> = store.neighbors(a, Direction::Outgoing).collect();
assert_eq!(outgoing.len(), 2);
assert!(outgoing.contains(&b));
assert!(outgoing.contains(&c));
let incoming: Vec<_> = store.neighbors(b, Direction::Incoming).collect();
assert_eq!(incoming.len(), 1);
assert!(incoming.contains(&a));
}
#[test]
fn test_nodes_by_label() {
let store = LpgStore::new().unwrap();
let p1 = store.create_node(&["Person"]);
let p2 = store.create_node(&["Person"]);
let _a = store.create_node(&["Animal"]);
let persons = store.nodes_by_label("Person");
assert_eq!(persons.len(), 2);
assert!(persons.contains(&p1));
assert!(persons.contains(&p2));
let animals = store.nodes_by_label("Animal");
assert_eq!(animals.len(), 1);
}
#[test]
fn test_delete_edge() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let edge_id = store.create_edge(a, b, "KNOWS");
assert_eq!(store.edge_count(), 1);
assert!(store.delete_edge(edge_id));
assert_eq!(store.edge_count(), 0);
assert!(store.get_edge(edge_id).is_none());
}
#[test]
fn test_lpg_store_config() {
let config = LpgStoreConfig {
backward_edges: false,
initial_node_capacity: 100,
initial_edge_capacity: 200,
};
let store = LpgStore::with_config(config).unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
store.create_edge(a, b, "KNOWS");
let outgoing: Vec<_> = store.neighbors(a, Direction::Outgoing).collect();
assert_eq!(outgoing.len(), 1);
let incoming: Vec<_> = store.neighbors(b, Direction::Incoming).collect();
assert_eq!(incoming.len(), 0);
}
#[test]
fn test_epoch_management() {
let store = LpgStore::new().unwrap();
let epoch0 = store.current_epoch();
assert_eq!(epoch0.as_u64(), 0);
let epoch1 = store.new_epoch();
assert_eq!(epoch1.as_u64(), 1);
let current = store.current_epoch();
assert_eq!(current.as_u64(), 1);
}
#[test]
fn test_node_properties() {
let store = LpgStore::new().unwrap();
let id = store.create_node(&["Person"]);
store.set_node_property(id, "name", Value::from("Alix"));
let name = store.get_node_property(id, &"name".into());
assert!(matches!(name, Some(Value::String(s)) if s.as_str() == "Alix"));
store.set_node_property(id, "name", Value::from("Gus"));
let name = store.get_node_property(id, &"name".into());
assert!(matches!(name, Some(Value::String(s)) if s.as_str() == "Gus"));
let old = store.remove_node_property(id, "name");
assert!(matches!(old, Some(Value::String(s)) if s.as_str() == "Gus"));
let name = store.get_node_property(id, &"name".into());
assert!(name.is_none());
let none = store.remove_node_property(id, "nonexistent");
assert!(none.is_none());
}
#[test]
fn test_edge_properties() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let edge_id = store.create_edge(a, b, "KNOWS");
store.set_edge_property(edge_id, "since", Value::from(2020i64));
let since = store.get_edge_property(edge_id, &"since".into());
assert_eq!(since.and_then(|v| v.as_int64()), Some(2020));
let old = store.remove_edge_property(edge_id, "since");
assert_eq!(old.and_then(|v| v.as_int64()), Some(2020));
let since = store.get_edge_property(edge_id, &"since".into());
assert!(since.is_none());
}
#[test]
fn test_add_remove_label() {
let store = LpgStore::new().unwrap();
let id = store.create_node(&["Person"]);
assert!(store.add_label(id, "Employee"));
let node = store.get_node(id).unwrap();
assert!(node.has_label("Person"));
assert!(node.has_label("Employee"));
assert!(!store.add_label(id, "Employee"));
assert!(store.remove_label(id, "Employee"));
let node = store.get_node(id).unwrap();
assert!(node.has_label("Person"));
assert!(!node.has_label("Employee"));
assert!(!store.remove_label(id, "Employee"));
assert!(!store.remove_label(id, "NonExistent"));
}
#[test]
fn test_add_label_to_nonexistent_node() {
let store = LpgStore::new().unwrap();
let fake_id = NodeId::new(999);
assert!(!store.add_label(fake_id, "Label"));
}
#[test]
fn test_remove_label_from_nonexistent_node() {
let store = LpgStore::new().unwrap();
let fake_id = NodeId::new(999);
assert!(!store.remove_label(fake_id, "Label"));
}
#[test]
fn test_node_ids() {
let store = LpgStore::new().unwrap();
let n1 = store.create_node(&["Person"]);
let n2 = store.create_node(&["Person"]);
let n3 = store.create_node(&["Person"]);
let ids = store.node_ids();
assert_eq!(ids.len(), 3);
assert!(ids.contains(&n1));
assert!(ids.contains(&n2));
assert!(ids.contains(&n3));
store.delete_node(n2);
let ids = store.node_ids();
assert_eq!(ids.len(), 2);
assert!(!ids.contains(&n2));
}
#[test]
fn test_delete_node_nonexistent() {
let store = LpgStore::new().unwrap();
let fake_id = NodeId::new(999);
assert!(!store.delete_node(fake_id));
}
#[test]
fn test_delete_edge_nonexistent() {
let store = LpgStore::new().unwrap();
let fake_id = EdgeId::new(999);
assert!(!store.delete_edge(fake_id));
}
#[test]
fn test_delete_edge_double() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let edge_id = store.create_edge(a, b, "KNOWS");
assert!(store.delete_edge(edge_id));
assert!(!store.delete_edge(edge_id)); }
#[test]
fn test_create_edge_with_props() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let edge_id = store.create_edge_with_props(
a,
b,
"KNOWS",
[
("since", Value::from(2020i64)),
("weight", Value::from(1.0)),
],
);
let edge = store.get_edge(edge_id).unwrap();
assert_eq!(
edge.get_property("since").and_then(|v| v.as_int64()),
Some(2020)
);
assert_eq!(
edge.get_property("weight").and_then(|v| v.as_float64()),
Some(1.0)
);
}
#[test]
fn test_delete_node_edges() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let c = store.create_node(&["Person"]);
store.create_edge(a, b, "KNOWS"); store.create_edge(c, a, "KNOWS");
assert_eq!(store.edge_count(), 2);
store.delete_node_edges(a);
assert_eq!(store.edge_count(), 0);
}
#[test]
fn test_neighbors_both_directions() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let c = store.create_node(&["Person"]);
store.create_edge(a, b, "KNOWS"); store.create_edge(c, a, "KNOWS");
let neighbors: Vec<_> = store.neighbors(a, Direction::Both).collect();
assert_eq!(neighbors.len(), 2);
assert!(neighbors.contains(&b)); assert!(neighbors.contains(&c)); }
#[test]
fn test_edges_from() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let c = store.create_node(&["Person"]);
let e1 = store.create_edge(a, b, "KNOWS");
let e2 = store.create_edge(a, c, "KNOWS");
let edges: Vec<_> = store.edges_from(a, Direction::Outgoing).collect();
assert_eq!(edges.len(), 2);
assert!(edges.iter().any(|(_, e)| *e == e1));
assert!(edges.iter().any(|(_, e)| *e == e2));
let incoming: Vec<_> = store.edges_from(b, Direction::Incoming).collect();
assert_eq!(incoming.len(), 1);
assert_eq!(incoming[0].1, e1);
}
#[test]
fn test_edges_to() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let c = store.create_node(&["Person"]);
let e1 = store.create_edge(a, b, "KNOWS");
let e2 = store.create_edge(c, b, "KNOWS");
let to_b = store.edges_to(b);
assert_eq!(to_b.len(), 2);
assert!(to_b.iter().any(|(src, e)| *src == a && *e == e1));
assert!(to_b.iter().any(|(src, e)| *src == c && *e == e2));
}
#[test]
fn test_out_degree_in_degree() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let c = store.create_node(&["Person"]);
store.create_edge(a, b, "KNOWS");
store.create_edge(a, c, "KNOWS");
store.create_edge(c, b, "KNOWS");
assert_eq!(store.out_degree(a), 2);
assert_eq!(store.out_degree(b), 0);
assert_eq!(store.out_degree(c), 1);
assert_eq!(store.in_degree(a), 0);
assert_eq!(store.in_degree(b), 2);
assert_eq!(store.in_degree(c), 1);
}
#[test]
fn test_edge_type() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let edge_id = store.create_edge(a, b, "KNOWS");
let edge_type = store.edge_type(edge_id);
assert_eq!(edge_type.as_deref(), Some("KNOWS"));
let fake_id = EdgeId::new(999);
assert!(store.edge_type(fake_id).is_none());
}
#[test]
fn test_count_methods() {
let store = LpgStore::new().unwrap();
assert_eq!(store.label_count(), 0);
assert_eq!(store.edge_type_count(), 0);
assert_eq!(store.property_key_count(), 0);
let a = store.create_node_with_props(&["Person"], [("age", Value::from(30i64))]);
let b = store.create_node(&["Company"]);
store.create_edge_with_props(a, b, "WORKS_AT", [("since", Value::from(2020i64))]);
assert_eq!(store.label_count(), 2); assert_eq!(store.edge_type_count(), 1); assert_eq!(store.property_key_count(), 2); }
#[test]
fn test_all_nodes_and_edges() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
store.create_edge(a, b, "KNOWS");
let nodes: Vec<_> = store.all_nodes().collect();
assert_eq!(nodes.len(), 2);
let edges: Vec<_> = store.all_edges().collect();
assert_eq!(edges.len(), 1);
}
#[test]
fn test_all_labels_and_edge_types() {
let store = LpgStore::new().unwrap();
store.create_node(&["Person"]);
store.create_node(&["Company"]);
let a = store.create_node(&["Animal"]);
let b = store.create_node(&["Animal"]);
store.create_edge(a, b, "EATS");
let labels = store.all_labels();
assert_eq!(labels.len(), 3);
assert!(labels.contains(&"Person".to_string()));
assert!(labels.contains(&"Company".to_string()));
assert!(labels.contains(&"Animal".to_string()));
let edge_types = store.all_edge_types();
assert_eq!(edge_types.len(), 1);
assert!(edge_types.contains(&"EATS".to_string()));
}
#[test]
fn test_all_property_keys() {
let store = LpgStore::new().unwrap();
let a = store.create_node_with_props(&["Person"], [("name", Value::from("Alix"))]);
let b = store.create_node_with_props(&["Person"], [("age", Value::from(30i64))]);
store.create_edge_with_props(a, b, "KNOWS", [("since", Value::from(2020i64))]);
let keys = store.all_property_keys();
assert!(keys.contains(&"name".to_string()));
assert!(keys.contains(&"age".to_string()));
assert!(keys.contains(&"since".to_string()));
}
#[test]
fn test_nodes_with_label() {
let store = LpgStore::new().unwrap();
store.create_node(&["Person"]);
store.create_node(&["Person"]);
store.create_node(&["Company"]);
let persons: Vec<_> = store.nodes_with_label("Person").collect();
assert_eq!(persons.len(), 2);
let companies: Vec<_> = store.nodes_with_label("Company").collect();
assert_eq!(companies.len(), 1);
let none: Vec<_> = store.nodes_with_label("NonExistent").collect();
assert_eq!(none.len(), 0);
}
#[test]
fn test_edges_with_type() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let c = store.create_node(&["Company"]);
store.create_edge(a, b, "KNOWS");
store.create_edge(a, c, "WORKS_AT");
let knows: Vec<_> = store.edges_with_type("KNOWS").collect();
assert_eq!(knows.len(), 1);
let works_at: Vec<_> = store.edges_with_type("WORKS_AT").collect();
assert_eq!(works_at.len(), 1);
let none: Vec<_> = store.edges_with_type("NonExistent").collect();
assert_eq!(none.len(), 0);
}
#[test]
fn test_nodes_by_label_nonexistent() {
let store = LpgStore::new().unwrap();
store.create_node(&["Person"]);
let empty = store.nodes_by_label("NonExistent");
assert!(empty.is_empty());
}
#[test]
fn test_statistics() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let c = store.create_node(&["Company"]);
store.create_edge(a, b, "KNOWS");
store.create_edge(a, c, "WORKS_AT");
store.compute_statistics();
let stats = store.statistics();
assert_eq!(stats.total_nodes, 3);
assert_eq!(stats.total_edges, 2);
let person_card = store.estimate_label_cardinality("Person");
assert!(person_card > 0.0);
let avg_degree = store.estimate_avg_degree("KNOWS", true);
assert!(avg_degree >= 0.0);
}
#[test]
fn test_zone_maps() {
let store = LpgStore::new().unwrap();
store.create_node_with_props(&["Person"], [("age", Value::from(25i64))]);
store.create_node_with_props(&["Person"], [("age", Value::from(35i64))]);
let might_match =
store.node_property_might_match(&"age".into(), CompareOp::Eq, &Value::from(30i64));
assert!(might_match);
let zone = store.node_property_zone_map(&"age".into());
assert!(zone.is_some());
let no_zone = store.node_property_zone_map(&"nonexistent".into());
assert!(no_zone.is_none());
let a = store.create_node(&["A"]);
let b = store.create_node(&["B"]);
store.create_edge_with_props(a, b, "REL", [("weight", Value::from(1.0))]);
let edge_zone = store.edge_property_zone_map(&"weight".into());
assert!(edge_zone.is_some());
}
#[test]
fn test_rebuild_zone_maps() {
let store = LpgStore::new().unwrap();
store.create_node_with_props(&["Person"], [("age", Value::from(25i64))]);
store.rebuild_zone_maps();
}
#[test]
fn test_create_node_with_id() {
let store = LpgStore::new().unwrap();
let specific_id = NodeId::new(100);
store
.create_node_with_id(specific_id, &["Person", "Employee"])
.unwrap();
let node = store.get_node(specific_id).unwrap();
assert!(node.has_label("Person"));
assert!(node.has_label("Employee"));
let next = store.create_node(&["Other"]);
assert!(next.as_u64() > 100);
}
#[test]
fn test_create_edge_with_id() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["A"]);
let b = store.create_node(&["B"]);
let specific_id = EdgeId::new(500);
store.create_edge_with_id(specific_id, a, b, "REL").unwrap();
let edge = store.get_edge(specific_id).unwrap();
assert_eq!(edge.src, a);
assert_eq!(edge.dst, b);
assert_eq!(edge.edge_type.as_str(), "REL");
let next = store.create_edge(a, b, "OTHER");
assert!(next.as_u64() > 500);
}
#[test]
fn test_set_epoch() {
let store = LpgStore::new().unwrap();
assert_eq!(store.current_epoch().as_u64(), 0);
store.set_epoch(EpochId::new(42));
assert_eq!(store.current_epoch().as_u64(), 42);
}
#[test]
fn test_get_node_nonexistent() {
let store = LpgStore::new().unwrap();
let fake_id = NodeId::new(999);
assert!(store.get_node(fake_id).is_none());
}
#[test]
fn test_get_edge_nonexistent() {
let store = LpgStore::new().unwrap();
let fake_id = EdgeId::new(999);
assert!(store.get_edge(fake_id).is_none());
}
#[test]
fn test_multiple_labels() {
let store = LpgStore::new().unwrap();
let id = store.create_node(&["Person", "Employee", "Manager"]);
let node = store.get_node(id).unwrap();
assert!(node.has_label("Person"));
assert!(node.has_label("Employee"));
assert!(node.has_label("Manager"));
assert!(!node.has_label("Other"));
}
#[test]
fn test_new_store_is_empty() {
let store = LpgStore::new().unwrap();
assert_eq!(store.node_count(), 0);
assert_eq!(store.edge_count(), 0);
}
#[test]
fn test_edges_from_both_directions() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["A"]);
let b = store.create_node(&["B"]);
let c = store.create_node(&["C"]);
let e1 = store.create_edge(a, b, "R1"); let e2 = store.create_edge(c, a, "R2");
let edges: Vec<_> = store.edges_from(a, Direction::Both).collect();
assert_eq!(edges.len(), 2);
assert!(edges.iter().any(|(_, e)| *e == e1)); assert!(edges.iter().any(|(_, e)| *e == e2)); }
#[test]
fn test_no_backward_adj_in_degree() {
let config = LpgStoreConfig {
backward_edges: false,
initial_node_capacity: 10,
initial_edge_capacity: 10,
};
let store = LpgStore::with_config(config).unwrap();
let a = store.create_node(&["A"]);
let b = store.create_node(&["B"]);
store.create_edge(a, b, "R");
let degree = store.in_degree(b);
assert_eq!(degree, 1);
}
#[test]
fn test_no_backward_adj_edges_to() {
let config = LpgStoreConfig {
backward_edges: false,
initial_node_capacity: 10,
initial_edge_capacity: 10,
};
let store = LpgStore::with_config(config).unwrap();
let a = store.create_node(&["A"]);
let b = store.create_node(&["B"]);
let e = store.create_edge(a, b, "R");
let edges = store.edges_to(b);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].1, e);
}
#[test]
fn test_node_versioned_creation() {
let store = LpgStore::new().unwrap();
let epoch = store.new_epoch();
let transaction_id = TransactionId::new(1);
let id = store.create_node_versioned(&["Person"], epoch, transaction_id);
assert!(store.get_node(id).is_some());
}
#[test]
fn test_edge_versioned_creation() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["A"]);
let b = store.create_node(&["B"]);
let epoch = store.new_epoch();
let transaction_id = TransactionId::new(1);
let edge_id = store.create_edge_versioned(a, b, "REL", epoch, transaction_id);
assert!(store.get_edge(edge_id).is_some());
}
#[test]
fn test_node_with_props_versioned() {
let store = LpgStore::new().unwrap();
let epoch = store.new_epoch();
let transaction_id = TransactionId::new(1);
let id = store.create_node_with_props_versioned(
&["Person"],
[("name", Value::from("Alix"))],
epoch,
transaction_id,
);
let node = store.get_node(id).unwrap();
assert_eq!(
node.get_property("name").and_then(|v| v.as_str()),
Some("Alix")
);
}
#[test]
fn test_discard_uncommitted_versions() {
let store = LpgStore::new().unwrap();
let epoch = store.new_epoch();
let transaction_id = TransactionId::new(42);
let node_id = store.create_node_versioned(&["Person"], epoch, transaction_id);
assert!(
store
.get_node_versioned(node_id, epoch, transaction_id)
.is_some(),
"Node should be visible to its own transaction"
);
store.discard_uncommitted_versions(transaction_id);
assert!(
store
.get_node_versioned(node_id, epoch, transaction_id)
.is_none(),
"Node should be gone after discard"
);
}
#[test]
fn test_property_index_create_and_lookup() {
let store = LpgStore::new().unwrap();
let alix = store.create_node(&["Person"]);
let gus = store.create_node(&["Person"]);
let vincent = store.create_node(&["Person"]);
store.set_node_property(alix, "city", Value::from("NYC"));
store.set_node_property(gus, "city", Value::from("NYC"));
store.set_node_property(vincent, "city", Value::from("LA"));
let nyc_people = store.find_nodes_by_property("city", &Value::from("NYC"));
assert_eq!(nyc_people.len(), 2);
store.create_property_index("city");
assert!(store.has_property_index("city"));
let nyc_people = store.find_nodes_by_property("city", &Value::from("NYC"));
assert_eq!(nyc_people.len(), 2);
assert!(nyc_people.contains(&alix));
assert!(nyc_people.contains(&gus));
let la_people = store.find_nodes_by_property("city", &Value::from("LA"));
assert_eq!(la_people.len(), 1);
assert!(la_people.contains(&vincent));
}
#[test]
fn test_property_index_maintained_on_update() {
let store = LpgStore::new().unwrap();
store.create_property_index("status");
let node = store.create_node(&["Task"]);
store.set_node_property(node, "status", Value::from("pending"));
let pending = store.find_nodes_by_property("status", &Value::from("pending"));
assert_eq!(pending.len(), 1);
assert!(pending.contains(&node));
store.set_node_property(node, "status", Value::from("done"));
let pending = store.find_nodes_by_property("status", &Value::from("pending"));
assert!(pending.is_empty());
let done = store.find_nodes_by_property("status", &Value::from("done"));
assert_eq!(done.len(), 1);
assert!(done.contains(&node));
}
#[test]
fn test_property_index_maintained_on_remove() {
let store = LpgStore::new().unwrap();
store.create_property_index("tag");
let node = store.create_node(&["Item"]);
store.set_node_property(node, "tag", Value::from("important"));
let found = store.find_nodes_by_property("tag", &Value::from("important"));
assert_eq!(found.len(), 1);
store.remove_node_property(node, "tag");
let found = store.find_nodes_by_property("tag", &Value::from("important"));
assert!(found.is_empty());
}
#[test]
fn test_property_index_drop() {
let store = LpgStore::new().unwrap();
store.create_property_index("key");
assert!(store.has_property_index("key"));
assert!(store.drop_property_index("key"));
assert!(!store.has_property_index("key"));
assert!(!store.drop_property_index("key"));
}
#[test]
fn test_property_index_multiple_values() {
let store = LpgStore::new().unwrap();
store.create_property_index("age");
let n1 = store.create_node(&["Person"]);
let n2 = store.create_node(&["Person"]);
let n3 = store.create_node(&["Person"]);
let n4 = store.create_node(&["Person"]);
store.set_node_property(n1, "age", Value::from(25i64));
store.set_node_property(n2, "age", Value::from(25i64));
store.set_node_property(n3, "age", Value::from(30i64));
store.set_node_property(n4, "age", Value::from(25i64));
let age_25 = store.find_nodes_by_property("age", &Value::from(25i64));
assert_eq!(age_25.len(), 3);
let age_30 = store.find_nodes_by_property("age", &Value::from(30i64));
assert_eq!(age_30.len(), 1);
let age_40 = store.find_nodes_by_property("age", &Value::from(40i64));
assert!(age_40.is_empty());
}
#[test]
fn test_property_index_builds_from_existing_data() {
let store = LpgStore::new().unwrap();
let n1 = store.create_node(&["Person"]);
let n2 = store.create_node(&["Person"]);
store.set_node_property(n1, "email", Value::from("alix@example.com"));
store.set_node_property(n2, "email", Value::from("gus@example.com"));
store.create_property_index("email");
let alix = store.find_nodes_by_property("email", &Value::from("alix@example.com"));
assert_eq!(alix.len(), 1);
assert!(alix.contains(&n1));
let gus = store.find_nodes_by_property("email", &Value::from("gus@example.com"));
assert_eq!(gus.len(), 1);
assert!(gus.contains(&n2));
}
#[test]
fn test_get_node_property_batch() {
let store = LpgStore::new().unwrap();
let n1 = store.create_node(&["Person"]);
let n2 = store.create_node(&["Person"]);
let n3 = store.create_node(&["Person"]);
store.set_node_property(n1, "age", Value::from(25i64));
store.set_node_property(n2, "age", Value::from(30i64));
let age_key = PropertyKey::new("age");
let values = store.get_node_property_batch(&[n1, n2, n3], &age_key);
assert_eq!(values.len(), 3);
assert_eq!(values[0], Some(Value::from(25i64)));
assert_eq!(values[1], Some(Value::from(30i64)));
assert_eq!(values[2], None);
}
#[test]
fn test_get_node_property_batch_empty() {
let store = LpgStore::new().unwrap();
let key = PropertyKey::new("any");
let values = store.get_node_property_batch(&[], &key);
assert!(values.is_empty());
}
#[test]
fn test_get_nodes_properties_batch() {
let store = LpgStore::new().unwrap();
let n1 = store.create_node(&["Person"]);
let n2 = store.create_node(&["Person"]);
let n3 = store.create_node(&["Person"]);
store.set_node_property(n1, "name", Value::from("Alix"));
store.set_node_property(n1, "age", Value::from(25i64));
store.set_node_property(n2, "name", Value::from("Gus"));
let all_props = store.get_nodes_properties_batch(&[n1, n2, n3]);
assert_eq!(all_props.len(), 3);
assert_eq!(all_props[0].len(), 2); assert_eq!(all_props[1].len(), 1); assert_eq!(all_props[2].len(), 0);
assert_eq!(
all_props[0].get(&PropertyKey::new("name")),
Some(&Value::from("Alix"))
);
assert_eq!(
all_props[1].get(&PropertyKey::new("name")),
Some(&Value::from("Gus"))
);
}
#[test]
fn test_get_nodes_properties_batch_empty() {
let store = LpgStore::new().unwrap();
let all_props = store.get_nodes_properties_batch(&[]);
assert!(all_props.is_empty());
}
#[test]
fn test_get_nodes_properties_selective_batch() {
let store = LpgStore::new().unwrap();
let n1 = store.create_node(&["Person"]);
let n2 = store.create_node(&["Person"]);
store.set_node_property(n1, "name", Value::from("Alix"));
store.set_node_property(n1, "age", Value::from(25i64));
store.set_node_property(n1, "email", Value::from("alix@example.com"));
store.set_node_property(n2, "name", Value::from("Gus"));
store.set_node_property(n2, "age", Value::from(30i64));
store.set_node_property(n2, "city", Value::from("NYC"));
let keys = vec![PropertyKey::new("name"), PropertyKey::new("age")];
let props = store.get_nodes_properties_selective_batch(&[n1, n2], &keys);
assert_eq!(props.len(), 2);
assert_eq!(props[0].len(), 2);
assert_eq!(
props[0].get(&PropertyKey::new("name")),
Some(&Value::from("Alix"))
);
assert_eq!(
props[0].get(&PropertyKey::new("age")),
Some(&Value::from(25i64))
);
assert_eq!(props[0].get(&PropertyKey::new("email")), None);
assert_eq!(props[1].len(), 2);
assert_eq!(
props[1].get(&PropertyKey::new("name")),
Some(&Value::from("Gus"))
);
assert_eq!(
props[1].get(&PropertyKey::new("age")),
Some(&Value::from(30i64))
);
assert_eq!(props[1].get(&PropertyKey::new("city")), None);
}
#[test]
fn test_get_nodes_properties_selective_batch_empty_keys() {
let store = LpgStore::new().unwrap();
let n1 = store.create_node(&["Person"]);
store.set_node_property(n1, "name", Value::from("Alix"));
let props = store.get_nodes_properties_selective_batch(&[n1], &[]);
assert_eq!(props.len(), 1);
assert!(props[0].is_empty()); }
#[test]
fn test_get_nodes_properties_selective_batch_missing_keys() {
let store = LpgStore::new().unwrap();
let n1 = store.create_node(&["Person"]);
store.set_node_property(n1, "name", Value::from("Alix"));
let keys = vec![PropertyKey::new("nonexistent"), PropertyKey::new("name")];
let props = store.get_nodes_properties_selective_batch(&[n1], &keys);
assert_eq!(props.len(), 1);
assert_eq!(props[0].len(), 1); assert_eq!(
props[0].get(&PropertyKey::new("name")),
Some(&Value::from("Alix"))
);
}
#[test]
fn test_find_nodes_in_range_inclusive() {
let store = LpgStore::new().unwrap();
let n1 = store.create_node_with_props(&["Person"], [("age", Value::from(20i64))]);
let n2 = store.create_node_with_props(&["Person"], [("age", Value::from(30i64))]);
let n3 = store.create_node_with_props(&["Person"], [("age", Value::from(40i64))]);
let _n4 = store.create_node_with_props(&["Person"], [("age", Value::from(50i64))]);
let result = store.find_nodes_in_range(
"age",
Some(&Value::from(20i64)),
Some(&Value::from(40i64)),
true,
true,
);
assert_eq!(result.len(), 3);
assert!(result.contains(&n1));
assert!(result.contains(&n2));
assert!(result.contains(&n3));
}
#[test]
fn test_find_nodes_in_range_exclusive() {
let store = LpgStore::new().unwrap();
store.create_node_with_props(&["Person"], [("age", Value::from(20i64))]);
let n2 = store.create_node_with_props(&["Person"], [("age", Value::from(30i64))]);
store.create_node_with_props(&["Person"], [("age", Value::from(40i64))]);
let result = store.find_nodes_in_range(
"age",
Some(&Value::from(20i64)),
Some(&Value::from(40i64)),
false,
false,
);
assert_eq!(result.len(), 1);
assert!(result.contains(&n2));
}
#[test]
fn test_find_nodes_in_range_open_ended() {
let store = LpgStore::new().unwrap();
store.create_node_with_props(&["Person"], [("age", Value::from(20i64))]);
store.create_node_with_props(&["Person"], [("age", Value::from(30i64))]);
let n3 = store.create_node_with_props(&["Person"], [("age", Value::from(40i64))]);
let n4 = store.create_node_with_props(&["Person"], [("age", Value::from(50i64))]);
let result = store.find_nodes_in_range("age", Some(&Value::from(35i64)), None, true, true);
assert_eq!(result.len(), 2);
assert!(result.contains(&n3));
assert!(result.contains(&n4));
let result = store.find_nodes_in_range("age", None, Some(&Value::from(25i64)), true, true);
assert_eq!(result.len(), 1);
}
#[test]
fn test_find_nodes_in_range_empty_result() {
let store = LpgStore::new().unwrap();
store.create_node_with_props(&["Person"], [("age", Value::from(20i64))]);
let result = store.find_nodes_in_range(
"age",
Some(&Value::from(100i64)),
Some(&Value::from(200i64)),
true,
true,
);
assert!(result.is_empty());
}
#[test]
fn test_find_nodes_in_range_nonexistent_property() {
let store = LpgStore::new().unwrap();
store.create_node_with_props(&["Person"], [("age", Value::from(20i64))]);
let result = store.find_nodes_in_range(
"weight",
Some(&Value::from(50i64)),
Some(&Value::from(100i64)),
true,
true,
);
assert!(result.is_empty());
}
#[test]
fn test_find_nodes_by_properties_multiple_conditions() {
let store = LpgStore::new().unwrap();
let alix = store.create_node_with_props(
&["Person"],
[("name", Value::from("Alix")), ("city", Value::from("NYC"))],
);
store.create_node_with_props(
&["Person"],
[("name", Value::from("Gus")), ("city", Value::from("NYC"))],
);
store.create_node_with_props(
&["Person"],
[("name", Value::from("Alix")), ("city", Value::from("LA"))],
);
let result = store
.find_nodes_by_properties(&[("name", Value::from("Alix")), ("city", Value::from("NYC"))]);
assert_eq!(result.len(), 1);
assert!(result.contains(&alix));
}
#[test]
fn test_find_nodes_by_properties_empty_conditions() {
let store = LpgStore::new().unwrap();
store.create_node(&["Person"]);
store.create_node(&["Person"]);
let result = store.find_nodes_by_properties(&[]);
assert_eq!(result.len(), 2);
}
#[test]
fn test_find_nodes_by_properties_no_match() {
let store = LpgStore::new().unwrap();
store.create_node_with_props(&["Person"], [("name", Value::from("Alix"))]);
let result = store.find_nodes_by_properties(&[("name", Value::from("Nobody"))]);
assert!(result.is_empty());
}
#[test]
fn test_find_nodes_by_properties_with_index() {
let store = LpgStore::new().unwrap();
store.create_property_index("name");
let alix = store.create_node_with_props(
&["Person"],
[("name", Value::from("Alix")), ("age", Value::from(30i64))],
);
store.create_node_with_props(
&["Person"],
[("name", Value::from("Gus")), ("age", Value::from(30i64))],
);
let result = store
.find_nodes_by_properties(&[("name", Value::from("Alix")), ("age", Value::from(30i64))]);
assert_eq!(result.len(), 1);
assert!(result.contains(&alix));
}
#[test]
fn test_estimate_label_cardinality() {
let store = LpgStore::new().unwrap();
store.create_node(&["Person"]);
store.create_node(&["Person"]);
store.create_node(&["Animal"]);
store.ensure_statistics_fresh();
let person_est = store.estimate_label_cardinality("Person");
let animal_est = store.estimate_label_cardinality("Animal");
let unknown_est = store.estimate_label_cardinality("Unknown");
assert!(
person_est >= 1.0,
"Person should have cardinality >= 1, got {person_est}"
);
assert!(
animal_est >= 1.0,
"Animal should have cardinality >= 1, got {animal_est}"
);
assert!(unknown_est >= 0.0);
}
#[test]
fn test_estimate_avg_degree() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["Person"]);
let b = store.create_node(&["Person"]);
let c = store.create_node(&["Person"]);
store.create_edge(a, b, "KNOWS");
store.create_edge(a, c, "KNOWS");
store.create_edge(b, c, "KNOWS");
store.ensure_statistics_fresh();
let outgoing = store.estimate_avg_degree("KNOWS", true);
let incoming = store.estimate_avg_degree("KNOWS", false);
assert!(
outgoing > 0.0,
"Outgoing degree should be > 0, got {outgoing}"
);
assert!(
incoming > 0.0,
"Incoming degree should be > 0, got {incoming}"
);
}
#[test]
fn test_delete_node_does_not_cascade() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["A"]);
let b = store.create_node(&["B"]);
let e = store.create_edge(a, b, "KNOWS");
assert!(store.delete_node(a));
assert!(store.get_node(a).is_none());
assert!(
store.get_edge(e).is_some(),
"Edge should survive non-detach node delete"
);
}
#[test]
fn test_delete_already_deleted_node() {
let store = LpgStore::new().unwrap();
let a = store.create_node(&["A"]);
assert!(store.delete_node(a));
assert!(!store.delete_node(a));
}
#[test]
fn test_delete_nonexistent_node() {
let store = LpgStore::new().unwrap();
assert!(!store.delete_node(NodeId::new(999)));
}
mod graph_store_traits {
use super::*;
use crate::graph::Direction;
use crate::graph::traits::{GraphStore, GraphStoreMut};
#[test]
fn trait_object_safety() {
let store: Arc<dyn GraphStoreMut> = Arc::new(LpgStore::new().unwrap());
let _read: &dyn GraphStore = &*store;
}
#[test]
fn trait_round_trip() {
let store = LpgStore::new().unwrap();
let store: &dyn GraphStoreMut = &store;
let alix = store.create_node(&["Person"]);
let gus = store.create_node(&["Person", "Developer"]);
store.set_node_property(alix, "name", Value::from("Alix"));
store.set_node_property(alix, "age", Value::from(30i64));
store.set_node_property(gus, "name", Value::from("Gus"));
let edge = store.create_edge(alix, gus, "KNOWS");
store.set_edge_property(edge, "since", Value::from(2020i64));
let read: &dyn GraphStore = store;
let alice_node = read.get_node(alix).expect("alix should exist");
assert!(alice_node.labels.contains(&arcstr::literal!("Person")));
let edge_data = read.get_edge(edge).expect("edge should exist");
assert_eq!(edge_data.src, alix);
assert_eq!(edge_data.dst, gus);
assert_eq!(
read.get_node_property(alix, &PropertyKey::new("name")),
Some(Value::from("Alix"))
);
assert_eq!(
read.get_edge_property(edge, &PropertyKey::new("since")),
Some(Value::from(2020i64))
);
let neighbors = read.neighbors(alix, Direction::Outgoing);
assert_eq!(neighbors, vec![gus]);
let edges = read.edges_from(alix, Direction::Outgoing);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0], (gus, edge));
assert_eq!(read.out_degree(alix), 1);
assert_eq!(read.in_degree(gus), 1);
assert_eq!(read.node_count(), 2);
assert_eq!(read.edge_count(), 1);
assert_eq!(read.nodes_by_label("Person").len(), 2);
assert_eq!(read.node_ids().len(), 2);
assert_eq!(read.edge_type(edge), Some(arcstr::literal!("KNOWS")));
let found = read.find_nodes_by_property("name", &Value::from("Alix"));
assert_eq!(found, vec![alix]);
}
#[test]
fn trait_mutation_operations() {
let store = LpgStore::new().unwrap();
let store: &dyn GraphStoreMut = &store;
let node = store.create_node(&["A"]);
assert!(store.add_label(node, "B"));
assert!(store.remove_label(node, "B"));
store.set_node_property(node, "key", Value::from("val"));
let removed = store.remove_node_property(node, "key");
assert_eq!(removed, Some(Value::from("val")));
assert!(store.delete_node(node));
assert!(store.get_node(node).is_none());
}
#[test]
fn trait_batch_edges() {
let store = LpgStore::new().unwrap();
let store: &dyn GraphStoreMut = &store;
let a = store.create_node(&["N"]);
let b = store.create_node(&["N"]);
let c = store.create_node(&["N"]);
let ids = store.batch_create_edges(&[(a, b, "E"), (a, c, "E"), (b, c, "E")]);
assert_eq!(ids.len(), 3);
assert_eq!(store.edge_count(), 3);
}
}
#[test]
fn test_clear() {
let store = LpgStore::new().unwrap();
let n1 = store.create_node(&["Person"]);
let n2 = store.create_node(&["Person"]);
store.set_node_property(n1, "name", "Alix".into());
let _e = store.create_edge(n1, n2, "KNOWS");
store.set_edge_property(_e, "since", 2024.into());
assert_eq!(store.node_count(), 2);
assert_eq!(store.edge_count(), 1);
store.clear();
assert_eq!(store.node_count(), 0);
assert_eq!(store.edge_count(), 0);
let n3 = store.create_node(&["Animal"]);
assert_eq!(store.node_count(), 1);
assert!(store.get_node(n3).is_some());
}