use std::collections::HashMap;
use std::ops::Bound;
use interstellar::index::IndexBuilder;
use interstellar::storage::{Graph, GraphStorage};
use interstellar::value::{Value, VertexId};
#[test]
fn supports_indexes_returns_true_for_graph() {
let graph = Graph::new();
assert!(graph.supports_indexes());
}
#[test]
fn vertices_by_property_uses_index_for_equality() {
let graph = Graph::new();
for age in 20..30 {
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String(format!("Person{}", age)));
props.insert("age".to_string(), Value::Int(age));
graph.add_vertex("person", props);
}
graph
.create_index(
IndexBuilder::vertex()
.label("person")
.property("age")
.build()
.unwrap(),
)
.unwrap();
let snapshot = graph.snapshot();
let results: Vec<_> = snapshot
.vertices_by_property(Some("person"), "age", &Value::Int(25))
.collect();
assert_eq!(results.len(), 1);
assert_eq!(results[0].properties.get("age"), Some(&Value::Int(25)));
}
#[test]
fn vertices_by_property_without_index_falls_back_to_scan() {
let graph = Graph::new();
for age in 20..30 {
let mut props = HashMap::new();
props.insert("age".to_string(), Value::Int(age));
graph.add_vertex("person", props);
}
let snapshot = graph.snapshot();
let results: Vec<_> = snapshot
.vertices_by_property(Some("person"), "age", &Value::Int(25))
.collect();
assert_eq!(results.len(), 1);
}
#[test]
fn edges_by_property_uses_index() {
let graph = Graph::new();
let v1 = graph.add_vertex("person", HashMap::new());
let v2 = graph.add_vertex("person", HashMap::new());
let v3 = graph.add_vertex("person", HashMap::new());
for weight in 1..=5 {
let mut props = HashMap::new();
props.insert("weight".to_string(), Value::Int(weight));
graph.add_edge(v1, v2, "knows", props.clone()).unwrap();
graph.add_edge(v2, v3, "knows", props).unwrap();
}
graph
.create_index(
IndexBuilder::edge()
.label("knows")
.property("weight")
.build()
.unwrap(),
)
.unwrap();
let results: Vec<_> = graph
.edges_by_property(Some("knows"), "weight", &Value::Int(3))
.collect();
assert_eq!(results.len(), 2);
for edge in &results {
assert_eq!(edge.properties.get("weight"), Some(&Value::Int(3)));
}
}
#[test]
fn vertices_by_property_range_uses_btree_index() {
let graph = Graph::new();
for age in 18..65 {
let mut props = HashMap::new();
props.insert("age".to_string(), Value::Int(age));
graph.add_vertex("person", props);
}
graph
.create_index(
IndexBuilder::vertex()
.label("person")
.property("age")
.build()
.unwrap(),
)
.unwrap();
let results: Vec<_> = graph
.vertices_by_property_range(
Some("person"),
"age",
Bound::Included(&Value::Int(30)),
Bound::Excluded(&Value::Int(40)),
)
.collect();
assert_eq!(results.len(), 10);
for vertex in &results {
let age = vertex.properties.get("age").unwrap().as_i64().unwrap();
assert!(age >= 30 && age < 40);
}
}
#[test]
fn vertices_by_property_range_without_index_falls_back_to_scan() {
let graph = Graph::new();
for age in 18..65 {
let mut props = HashMap::new();
props.insert("age".to_string(), Value::Int(age));
graph.add_vertex("person", props);
}
let results: Vec<_> = graph
.vertices_by_property_range(
Some("person"),
"age",
Bound::Included(&Value::Int(30)),
Bound::Excluded(&Value::Int(40)),
)
.collect();
assert_eq!(results.len(), 10);
}
#[test]
fn traversal_can_access_indexed_storage_methods() {
let graph = Graph::new();
for age in 20..30 {
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String(format!("Person{}", age)));
props.insert("age".to_string(), Value::Int(age));
graph.add_vertex("person", props);
}
graph
.create_index(
IndexBuilder::vertex()
.label("person")
.property("age")
.build()
.unwrap(),
)
.unwrap();
assert!(graph.supports_indexes());
let results: Vec<_> = graph
.vertices_by_property(Some("person"), "age", &Value::Int(25))
.collect();
assert_eq!(results.len(), 1);
assert_eq!(
results[0].properties.get("name"),
Some(&Value::String("Person25".to_string()))
);
}
#[test]
fn index_maintained_with_graph() {
let graph = Graph::new();
graph
.create_index(
IndexBuilder::vertex()
.label("user")
.property("email")
.unique()
.build()
.unwrap(),
)
.unwrap();
let mut props = HashMap::new();
props.insert(
"email".to_string(),
Value::String("alice@example.com".into()),
);
graph.add_vertex("user", props);
let mut props = HashMap::new();
props.insert("email".to_string(), Value::String("bob@example.com".into()));
graph.add_vertex("user", props);
let snapshot = graph.snapshot();
let results: Vec<_> = snapshot
.vertices_by_property(
Some("user"),
"email",
&Value::String("alice@example.com".into()),
)
.collect();
assert_eq!(results.len(), 1);
}
#[test]
fn range_query_unbounded_start() {
let graph = Graph::new();
for value in 1..=10 {
let mut props = HashMap::new();
props.insert("value".to_string(), Value::Int(value));
graph.add_vertex("item", props);
}
graph
.create_index(
IndexBuilder::vertex()
.label("item")
.property("value")
.build()
.unwrap(),
)
.unwrap();
let results: Vec<_> = graph
.vertices_by_property_range(
Some("item"),
"value",
Bound::Unbounded,
Bound::Excluded(&Value::Int(5)),
)
.collect();
assert_eq!(results.len(), 4); }
#[test]
fn range_query_unbounded_end() {
let graph = Graph::new();
for value in 1..=10 {
let mut props = HashMap::new();
props.insert("value".to_string(), Value::Int(value));
graph.add_vertex("item", props);
}
graph
.create_index(
IndexBuilder::vertex()
.label("item")
.property("value")
.build()
.unwrap(),
)
.unwrap();
let results: Vec<_> = graph
.vertices_by_property_range(
Some("item"),
"value",
Bound::Included(&Value::Int(7)),
Bound::Unbounded,
)
.collect();
assert_eq!(results.len(), 4); }
#[test]
fn range_query_excluded_bounds() {
let graph = Graph::new();
for value in 1..=10 {
let mut props = HashMap::new();
props.insert("value".to_string(), Value::Int(value));
graph.add_vertex("item", props);
}
graph
.create_index(
IndexBuilder::vertex()
.label("item")
.property("value")
.build()
.unwrap(),
)
.unwrap();
let results: Vec<_> = graph
.vertices_by_property_range(
Some("item"),
"value",
Bound::Excluded(&Value::Int(3)),
Bound::Excluded(&Value::Int(8)),
)
.collect();
assert_eq!(results.len(), 4); }
#[test]
fn property_lookup_respects_label_filter() {
let graph = Graph::new();
for age in 20..25 {
let mut props = HashMap::new();
props.insert("age".to_string(), Value::Int(age));
graph.add_vertex("person", props.clone());
graph.add_vertex("robot", props);
}
graph
.create_index(
IndexBuilder::vertex()
.label("person")
.property("age")
.build()
.unwrap(),
)
.unwrap();
let people: Vec<_> = graph
.vertices_by_property(Some("person"), "age", &Value::Int(22))
.collect();
assert_eq!(people.len(), 1);
assert_eq!(people[0].label, "person");
let robots: Vec<_> = graph
.vertices_by_property(Some("robot"), "age", &Value::Int(22))
.collect();
assert_eq!(robots.len(), 1);
assert_eq!(robots[0].label, "robot");
}
#[test]
fn property_lookup_without_label_uses_all_labels_index() {
let graph = Graph::new();
for i in 1..=5 {
let mut props = HashMap::new();
props.insert("priority".to_string(), Value::Int(i));
graph.add_vertex("task", props.clone());
graph.add_vertex("bug", props);
}
graph
.create_index(IndexBuilder::vertex().property("priority").build().unwrap())
.unwrap();
let results: Vec<_> = graph
.vertices_by_property(None, "priority", &Value::Int(3))
.collect();
assert_eq!(results.len(), 2);
}
#[test]
fn unique_index_provides_fast_single_lookup() {
let graph = Graph::new();
graph
.create_index(
IndexBuilder::vertex()
.label("user")
.property("email")
.unique()
.build()
.unwrap(),
)
.unwrap();
for i in 1..=100 {
let mut props = HashMap::new();
props.insert(
"email".to_string(),
Value::String(format!("user{}@example.com", i)),
);
props.insert("name".to_string(), Value::String(format!("User {}", i)));
graph.add_vertex("user", props);
}
let results: Vec<_> = graph
.vertices_by_property(
Some("user"),
"email",
&Value::String("user50@example.com".into()),
)
.collect();
assert_eq!(results.len(), 1);
assert_eq!(
results[0].properties.get("name"),
Some(&Value::String("User 50".into()))
);
}
#[test]
fn indexed_lookup_on_large_graph() {
let graph = Graph::new();
for i in 0..10_000 {
let mut props = HashMap::new();
props.insert("index".to_string(), Value::Int(i));
props.insert(
"category".to_string(),
Value::String(format!("cat{}", i % 100)),
);
graph.add_vertex("item", props);
}
graph
.create_index(
IndexBuilder::vertex()
.label("item")
.property("index")
.build()
.unwrap(),
)
.unwrap();
let start = std::time::Instant::now();
for target in [0, 5000, 9999] {
let results: Vec<_> = graph
.vertices_by_property(Some("item"), "index", &Value::Int(target))
.collect();
assert_eq!(results.len(), 1);
}
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 100,
"Indexed lookup took too long: {:?}",
elapsed
);
}
#[test]
fn range_query_on_large_graph() {
let graph = Graph::new();
for i in 0..10_000i64 {
let mut props = HashMap::new();
props.insert("timestamp".to_string(), Value::Int(i));
graph.add_vertex("event", props);
}
graph
.create_index(
IndexBuilder::vertex()
.label("event")
.property("timestamp")
.build()
.unwrap(),
)
.unwrap();
let start = std::time::Instant::now();
let results: Vec<_> = graph
.vertices_by_property_range(
Some("event"),
"timestamp",
Bound::Included(&Value::Int(1000)),
Bound::Excluded(&Value::Int(2000)),
)
.collect();
let elapsed = start.elapsed();
assert_eq!(results.len(), 1000);
assert!(
elapsed.as_millis() < 100,
"Range query took too long: {:?}",
elapsed
);
}
#[test]
fn traversal_v_by_property_uses_index() {
let graph = Graph::new();
graph
.create_index(
IndexBuilder::vertex()
.label("person")
.property("name")
.unique()
.build()
.unwrap(),
)
.unwrap();
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String("Alice".into()));
props.insert("age".to_string(), Value::Int(30));
graph.add_vertex("person", props);
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String("Bob".into()));
props.insert("age".to_string(), Value::Int(25));
graph.add_vertex("person", props);
let snapshot = graph.snapshot();
let g = snapshot.gremlin();
let results = g
.v_by_property(Some("person"), "name", "Alice")
.values("age")
.to_list();
assert_eq!(results.len(), 1);
assert_eq!(results[0], Value::Int(30));
}
#[test]
fn traversal_v_by_property_range_uses_index() {
let graph = Graph::new();
graph
.create_index(
IndexBuilder::vertex()
.label("person")
.property("age")
.build()
.unwrap(),
)
.unwrap();
for (name, age) in [("Alice", 25), ("Bob", 30), ("Charlie", 35), ("Diana", 40)] {
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String(name.into()));
props.insert("age".to_string(), Value::Int(age));
graph.add_vertex("person", props);
}
let snapshot = graph.snapshot();
let g = snapshot.gremlin();
let results = g
.v_by_property_range(
Some("person"),
"age",
Bound::Included(&Value::Int(28)),
Bound::Included(&Value::Int(36)),
)
.values("name")
.to_list();
assert_eq!(results.len(), 2);
let names: Vec<_> = results.iter().filter_map(|v| v.as_str()).collect();
assert!(names.contains(&"Bob"));
assert!(names.contains(&"Charlie"));
}
#[test]
fn traversal_e_by_property_uses_index() {
let graph = Graph::new();
graph
.create_index(
IndexBuilder::edge()
.label("knows")
.property("since")
.build()
.unwrap(),
)
.unwrap();
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String("Alice".into()));
let alice = graph.add_vertex("person", props);
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String("Bob".into()));
let bob = graph.add_vertex("person", props);
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String("Charlie".into()));
let charlie = graph.add_vertex("person", props);
let mut edge_props = HashMap::new();
edge_props.insert("since".to_string(), Value::Int(2020));
graph.add_edge(alice, bob, "knows", edge_props).unwrap();
let mut edge_props = HashMap::new();
edge_props.insert("since".to_string(), Value::Int(2022));
graph.add_edge(bob, charlie, "knows", edge_props).unwrap();
let snapshot = graph.snapshot();
let g = snapshot.gremlin();
let results = g
.e_by_property(Some("knows"), "since", 2020i64)
.in_v()
.values("name")
.to_list();
assert_eq!(results.len(), 1);
assert_eq!(results[0], Value::String("Bob".into()));
}
#[test]
fn traversal_with_index_chains_complex_queries() {
let graph = Graph::new();
graph
.create_index(
IndexBuilder::vertex()
.label("person")
.property("department")
.build()
.unwrap(),
)
.unwrap();
let departments = [
"Engineering",
"Marketing",
"Engineering",
"Sales",
"Engineering",
];
let names = ["Alice", "Bob", "Charlie", "Diana", "Eve"];
let ages = [30, 25, 35, 28, 32];
for (i, ((name, dept), age)) in names
.iter()
.zip(departments.iter())
.zip(ages.iter())
.enumerate()
{
let mut props = HashMap::new();
props.insert("name".to_string(), Value::String((*name).into()));
props.insert("department".to_string(), Value::String((*dept).into()));
props.insert("age".to_string(), Value::Int(*age));
let id = graph.add_vertex("person", props);
if i > 0 {
let prev = VertexId((i - 1) as u64);
graph
.add_edge(prev, id, "works_with", HashMap::new())
.unwrap();
}
}
let snapshot = graph.snapshot();
let g = snapshot.gremlin();
let results = g
.v_by_property(Some("person"), "department", "Engineering")
.out_labels(&["works_with"])
.has_where("age", interstellar::traversal::p::gt(30))
.values("name")
.dedup()
.to_list();
assert!(results.is_empty());
let count = g
.v_by_property(Some("person"), "department", "Engineering")
.count();
assert_eq!(count, 3); }