use grafeo_common::types::{NodeId, PropertyKey, Value};
use grafeo_engine::GrafeoDB;
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
fn db() -> GrafeoDB {
GrafeoDB::new_in_memory()
}
mod batch_create_with_props {
use super::*;
#[test]
fn creates_nodes_with_mixed_properties() {
let db = db();
let mut props_list = Vec::new();
let mut p1 = HashMap::new();
p1.insert(PropertyKey::new("text"), Value::String("hello".into()));
p1.insert(PropertyKey::new("user_id"), Value::String("u1".into()));
p1.insert(PropertyKey::new("score"), Value::Float64(0.95));
props_list.push(p1);
let mut p2 = HashMap::new();
p2.insert(PropertyKey::new("text"), Value::String("world".into()));
p2.insert(PropertyKey::new("user_id"), Value::String("u1".into()));
p2.insert(PropertyKey::new("score"), Value::Float64(0.80));
props_list.push(p2);
let ids = db.batch_create_nodes_with_props("Memory", props_list);
assert_eq!(ids.len(), 2);
let node = db.get_node(ids[0]).unwrap();
assert_eq!(
node.properties.get(&PropertyKey::new("text")),
Some(&Value::String("hello".into()))
);
assert_eq!(
node.properties.get(&PropertyKey::new("user_id")),
Some(&Value::String("u1".into()))
);
}
#[test]
fn creates_nodes_with_vector_properties() {
let db = db();
let mut props_list = Vec::new();
let mut p1 = HashMap::new();
p1.insert(PropertyKey::new("text"), Value::String("doc1".into()));
p1.insert(
PropertyKey::new("embedding"),
Value::Vector(vec![1.0, 0.0, 0.0].into()),
);
props_list.push(p1);
let mut p2 = HashMap::new();
p2.insert(PropertyKey::new("text"), Value::String("doc2".into()));
p2.insert(
PropertyKey::new("embedding"),
Value::Vector(vec![0.0, 1.0, 0.0].into()),
);
props_list.push(p2);
let ids = db.batch_create_nodes_with_props("Document", props_list);
assert_eq!(ids.len(), 2);
let node = db.get_node(ids[0]).unwrap();
match node.properties.get(&PropertyKey::new("embedding")) {
Some(Value::Vector(v)) => assert_eq!(v.len(), 3),
other => panic!("Expected Vector, got {:?}", other),
}
}
#[test]
fn empty_list_returns_empty_ids() {
let db = db();
let ids = db.batch_create_nodes_with_props("Memory", Vec::new());
assert!(ids.is_empty());
}
#[test]
fn nodes_with_different_property_sets() {
let db = db();
let mut props_list = Vec::new();
let mut p1 = HashMap::new();
p1.insert(PropertyKey::new("text"), Value::String("short".into()));
p1.insert(PropertyKey::new("type"), Value::String("note".into()));
props_list.push(p1);
let mut p2 = HashMap::new();
p2.insert(PropertyKey::new("text"), Value::String("detailed".into()));
p2.insert(PropertyKey::new("type"), Value::String("memo".into()));
p2.insert(PropertyKey::new("user_id"), Value::String("u1".into()));
p2.insert(PropertyKey::new("priority"), Value::Int64(1));
p2.insert(PropertyKey::new("archived"), Value::Bool(false));
props_list.push(p2);
let ids = db.batch_create_nodes_with_props("Item", props_list);
assert_eq!(ids.len(), 2);
let n1 = db.get_node(ids[0]).unwrap();
let n2 = db.get_node(ids[1]).unwrap();
assert_eq!(n1.properties.len(), 2);
assert_eq!(n2.properties.len(), 5);
}
#[cfg(feature = "vector-index")]
#[test]
fn auto_inserts_into_vector_index() {
let db = db();
db.create_vector_index("Doc", "emb", Some(3), None, None, None)
.unwrap();
let mut props_list = Vec::new();
let mut p1 = HashMap::new();
p1.insert(
PropertyKey::new("emb"),
Value::Vector(vec![1.0, 0.0, 0.0].into()),
);
props_list.push(p1);
let mut p2 = HashMap::new();
p2.insert(
PropertyKey::new("emb"),
Value::Vector(vec![0.0, 1.0, 0.0].into()),
);
props_list.push(p2);
let ids = db.batch_create_nodes_with_props("Doc", props_list);
assert_eq!(ids.len(), 2);
let results = db
.vector_search("Doc", "emb", &[1.0, 0.0, 0.0], 10, None, None)
.unwrap();
assert_eq!(results.len(), 2);
}
}
#[cfg(feature = "vector-index")]
mod filter_optimization {
use super::*;
fn make_memory(text: &str, user: &str, ts: i64, emb: Vec<f32>) -> HashMap<PropertyKey, Value> {
let mut p = HashMap::new();
p.insert(PropertyKey::new("text"), Value::String(text.into()));
p.insert(PropertyKey::new("user_id"), Value::String(user.into()));
p.insert(PropertyKey::new("created_at"), Value::Int64(ts));
p.insert(PropertyKey::new("embedding"), Value::Vector(emb.into()));
p
}
#[test]
fn operator_filter_gte_works() {
let db = db();
db.create_vector_index("Memory", "embedding", Some(3), None, None, None)
.unwrap();
db.batch_create_nodes_with_props(
"Memory",
vec![
make_memory("old", "u1", 1000, vec![1.0, 0.0, 0.0]),
make_memory("new", "u1", 2000, vec![0.0, 1.0, 0.0]),
make_memory("newest", "u1", 3000, vec![0.0, 0.0, 1.0]),
],
);
let mut filters = HashMap::new();
let mut gte_map = BTreeMap::new();
gte_map.insert(PropertyKey::new("$gte"), Value::Int64(2000));
filters.insert("created_at".to_string(), Value::Map(Arc::new(gte_map)));
let results = db
.vector_search(
"Memory",
"embedding",
&[1.0, 0.0, 0.0],
10,
None,
Some(&filters),
)
.unwrap();
assert_eq!(
results.len(),
2,
"Should find 2 memories with created_at >= 2000"
);
}
#[test]
fn combined_equality_and_operator_filter() {
let db = db();
db.create_vector_index("Memory", "embedding", Some(2), None, None, None)
.unwrap();
let mut p1 = HashMap::new();
p1.insert(PropertyKey::new("text"), Value::String("u1 old".into()));
p1.insert(PropertyKey::new("user_id"), Value::String("u1".into()));
p1.insert(PropertyKey::new("created_at"), Value::Int64(1000));
p1.insert(
PropertyKey::new("embedding"),
Value::Vector(vec![1.0, 0.0].into()),
);
let mut p2 = HashMap::new();
p2.insert(PropertyKey::new("text"), Value::String("u1 new".into()));
p2.insert(PropertyKey::new("user_id"), Value::String("u1".into()));
p2.insert(PropertyKey::new("created_at"), Value::Int64(2000));
p2.insert(
PropertyKey::new("embedding"),
Value::Vector(vec![0.0, 1.0].into()),
);
let mut p3 = HashMap::new();
p3.insert(PropertyKey::new("text"), Value::String("u2 new".into()));
p3.insert(PropertyKey::new("user_id"), Value::String("u2".into()));
p3.insert(PropertyKey::new("created_at"), Value::Int64(2000));
p3.insert(
PropertyKey::new("embedding"),
Value::Vector(vec![1.0, 1.0].into()),
);
db.batch_create_nodes_with_props("Memory", vec![p1, p2, p3]);
let mut filters = HashMap::new();
filters.insert("user_id".to_string(), Value::String("u1".into()));
let mut gte_map = BTreeMap::new();
gte_map.insert(PropertyKey::new("$gte"), Value::Int64(2000));
filters.insert("created_at".to_string(), Value::Map(Arc::new(gte_map)));
let results = db
.vector_search("Memory", "embedding", &[1.0, 0.0], 10, None, Some(&filters))
.unwrap();
assert_eq!(results.len(), 1, "Should find only u1's new memory");
}
#[test]
fn operator_filter_lt_works() {
let db = db();
db.create_vector_index("Memory", "embedding", Some(2), None, None, None)
.unwrap();
db.batch_create_nodes_with_props(
"Memory",
vec![
{
let mut p = HashMap::new();
p.insert(PropertyKey::new("score"), Value::Int64(10));
p.insert(
PropertyKey::new("embedding"),
Value::Vector(vec![1.0, 0.0].into()),
);
p
},
{
let mut p = HashMap::new();
p.insert(PropertyKey::new("score"), Value::Int64(50));
p.insert(
PropertyKey::new("embedding"),
Value::Vector(vec![0.0, 1.0].into()),
);
p
},
{
let mut p = HashMap::new();
p.insert(PropertyKey::new("score"), Value::Int64(90));
p.insert(
PropertyKey::new("embedding"),
Value::Vector(vec![1.0, 1.0].into()),
);
p
},
],
);
let mut filters = HashMap::new();
let mut lt_map = BTreeMap::new();
lt_map.insert(PropertyKey::new("$lt"), Value::Int64(50));
filters.insert("score".to_string(), Value::Map(Arc::new(lt_map)));
let results = db
.vector_search("Memory", "embedding", &[1.0, 0.0], 10, None, Some(&filters))
.unwrap();
assert_eq!(results.len(), 1, "Should find only score < 50");
}
}
#[cfg(feature = "temporal")]
mod temporal_versioning {
use super::*;
#[test]
fn get_node_property_at_epoch() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
let e1 = db.current_epoch();
s.execute("MATCH (p:Person {name: 'Alix'}) SET p.name = 'Alicia'")
.unwrap();
let e2 = db.current_epoch();
let val = db.get_node_property_at_epoch(NodeId(0), "name", e1);
assert_eq!(val, Some(Value::String("Alix".into())));
let val = db.get_node_property_at_epoch(NodeId(0), "name", e2);
assert_eq!(val, Some(Value::String("Alicia".into())));
}
#[test]
fn get_node_property_history() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix'})").unwrap();
s.execute("MATCH (p:Person {name: 'Alix'}) SET p.name = 'Alicia'")
.unwrap();
s.execute("MATCH (p:Person {name: 'Alicia'}) SET p.name = 'Ali'")
.unwrap();
let history = db.get_node_property_history(NodeId(0), "name");
assert_eq!(
history.len(),
3,
"Should have 3 versions: Alix, Alicia, Ali"
);
assert_eq!(history[0].1, Value::String("Alix".into()));
assert_eq!(history[1].1, Value::String("Alicia".into()));
assert_eq!(history[2].1, Value::String("Ali".into()));
}
#[test]
fn get_all_node_property_history() {
let db = db();
let s = db.session();
s.execute("INSERT (:Person {name: 'Alix', age: 30})")
.unwrap();
s.execute("MATCH (p:Person {name: 'Alix'}) SET p.age = 31")
.unwrap();
let all_history = db.get_all_node_property_history(NodeId(0));
let name_hist: Vec<_> = all_history
.iter()
.filter(|(k, _)| k.as_ref() == "name")
.collect();
let age_hist: Vec<_> = all_history
.iter()
.filter(|(k, _)| k.as_ref() == "age")
.collect();
assert_eq!(name_hist.len(), 1);
assert_eq!(name_hist[0].1.len(), 1, "name has 1 version");
assert_eq!(age_hist.len(), 1);
assert_eq!(age_hist[0].1.len(), 2, "age has 2 versions");
}
#[test]
fn property_at_epoch_returns_none_for_missing() {
let db = db();
db.session()
.execute("INSERT (:Person {name: 'Alix'})")
.unwrap();
let epoch = db.current_epoch();
let val = db.get_node_property_at_epoch(NodeId(0), "nonexistent", epoch);
assert_eq!(val, None);
let val = db.get_node_property_at_epoch(NodeId(999), "name", epoch);
assert_eq!(val, None);
}
#[test]
fn property_history_empty_for_nonexistent() {
let db = db();
let history = db.get_node_property_history(NodeId(999), "name");
assert!(history.is_empty());
}
#[test]
fn epochs_are_ascending() {
let db = db();
let s = db.session();
s.execute("INSERT (:Counter {val: 0})").unwrap();
s.execute("MATCH (c:Counter) SET c.val = 1").unwrap();
s.execute("MATCH (c:Counter) SET c.val = 2").unwrap();
let history = db.get_node_property_history(NodeId(0), "val");
let epochs: Vec<u64> = history.iter().map(|(e, _)| e.as_u64()).collect();
assert_eq!(epochs, {
let mut sorted = epochs.clone();
sorted.sort_unstable();
sorted
});
}
}