use sparrowdb::GraphDb;
use sparrowdb_execution::Value;
fn make_db() -> (tempfile::TempDir, GraphDb) {
let dir = tempfile::tempdir().expect("tempdir");
let db = GraphDb::open(dir.path()).expect("open db");
(dir, db)
}
#[test]
fn create_vector_index_ddl() {
let (_dir, db) = make_db();
db.execute(
"CREATE VECTOR INDEX FOR (n:Memory) ON (n.embedding) \
OPTIONS { dimensions: 4, similarity: 'cosine' }",
)
.expect("CREATE VECTOR INDEX must succeed");
db.execute(
"CREATE VECTOR INDEX FOR (n:Memory) ON (n.embedding) \
OPTIONS { dimensions: 4, similarity: 'cosine' }",
)
.expect("duplicate CREATE VECTOR INDEX must be a no-op");
}
#[test]
fn create_vector_index_api() {
let (_dir, db) = make_db();
db.create_vector_index("Person", "emb", 3, "cosine")
.expect("create_vector_index must succeed");
assert!(
db.get_vector_index("Person", "emb").is_some(),
"index must be registered"
);
}
#[test]
fn drop_vector_index_api() {
let (_dir, db) = make_db();
db.create_vector_index("Person", "emb", 3, "cosine")
.expect("create");
db.drop_vector_index("Person", "emb")
.expect("drop_vector_index must succeed");
assert!(
db.get_vector_index("Person", "emb").is_none(),
"index must be gone after drop"
);
}
#[test]
fn merge_with_params_inserts_into_vector_index() {
let (_dir, db) = make_db();
db.create_vector_index("Memory", "embedding", 3, "cosine")
.expect("create index");
let arc = db
.get_vector_index("Memory", "embedding")
.expect("index exists");
arc.write()
.expect("write lock")
.insert(1, &[1.0_f32, 0.0, 0.0]);
arc.write()
.expect("write lock")
.insert(2, &[0.0_f32, 1.0, 0.0]);
let idx = arc.read().expect("read lock");
let results = idx.search(&[1.0_f32, 0.0, 0.0], 5, 10);
assert!(
!results.is_empty(),
"HNSW search must return at least one result"
);
assert_eq!(results[0].0, 1, "nearest to [1,0,0] must be node 1");
}
#[test]
fn vector_similarity_function() {
let (_dir, db) = make_db();
db.execute("CREATE (a:Vec {x: 1.0, y: 0.0, z: 0.0, id: 1})")
.expect("create a");
db.execute("CREATE (b:Vec {x: 0.0, y: 1.0, z: 0.0, id: 2})")
.expect("create b");
let res = db
.execute("RETURN vector_similarity([1.0, 0.0, 0.0], [1.0, 0.0, 0.0]) AS sim")
.expect("vector_similarity must execute");
assert_eq!(res.rows.len(), 1);
if let Value::Float64(sim) = &res.rows[0][0] {
assert!(
(sim - 1.0).abs() < 1e-5,
"cosine similarity of identical vectors must be 1.0, got {sim}"
);
} else {
panic!("expected Float64, got {:?}", res.rows[0][0]);
}
}
#[test]
fn vector_similarity_orthogonal_is_zero() {
let (_dir, db) = make_db();
let res = db
.execute("RETURN vector_similarity([1.0, 0.0], [0.0, 1.0]) AS sim")
.expect("execute");
assert_eq!(res.rows.len(), 1);
if let Value::Float64(sim) = &res.rows[0][0] {
assert!(
sim.abs() < 1e-5,
"cosine similarity of orthogonal vectors must be ~0, got {sim}"
);
} else {
panic!("expected Float64, got {:?}", res.rows[0][0]);
}
}
#[test]
fn vector_distance_function() {
let (_dir, db) = make_db();
let res = db
.execute("RETURN vector_distance([0.0, 0.0], [3.0, 4.0]) AS d")
.expect("execute");
assert_eq!(res.rows.len(), 1);
if let Value::Float64(d) = &res.rows[0][0] {
assert!(
(d - 5.0).abs() < 1e-4,
"Euclidean distance from (0,0) to (3,4) must be 5.0, got {d}"
);
} else {
panic!("expected Float64, got {:?}", res.rows[0][0]);
}
}
#[test]
fn vector_dot_function() {
let (_dir, db) = make_db();
let res = db
.execute("RETURN vector_dot([2.0, 3.0], [4.0, 5.0]) AS dp")
.expect("execute");
assert_eq!(res.rows.len(), 1);
if let Value::Float64(dp) = &res.rows[0][0] {
assert!(
(dp - 23.0).abs() < 1e-4,
"dot product of [2,3]·[4,5] must be 23, got {dp}"
);
} else {
panic!("expected Float64, got {:?}", res.rows[0][0]);
}
}
#[test]
fn vector_index_survives_restart() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().to_path_buf();
{
let db = GraphDb::open(&path).expect("open");
db.create_vector_index("Memory", "embedding", 3, "cosine")
.expect("create index");
let arc = db.get_vector_index("Memory", "embedding").expect("index");
arc.write().expect("write").insert(42, &[1.0_f32, 0.0, 0.0]);
let vidx_dir = path.join("vector_indexes");
arc.read()
.expect("read")
.save(&vidx_dir, "Memory", "embedding")
.expect("save");
}
{
let db = GraphDb::open(&path).expect("re-open");
let arc = db
.get_vector_index("Memory", "embedding")
.expect("index must survive restart");
let idx = arc.read().expect("read");
let results = idx.search(&[1.0_f32, 0.0, 0.0], 5, 10);
assert!(
!results.is_empty(),
"inserted node must be found after restart"
);
assert_eq!(results[0].0, 42, "node_id 42 must be the nearest neighbour");
}
}
#[test]
fn hnsw_bulk_insert_and_top_k() {
let (_dir, db) = make_db();
db.create_vector_index("Item", "vec", 8, "cosine")
.expect("create index");
let arc = db.get_vector_index("Item", "vec").expect("index");
for i in 0u64..50 {
let v: Vec<f32> = (0..8)
.map(|d| ((i * 7 + d * 3) % 17) as f32 / 17.0)
.collect();
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-9);
let vn: Vec<f32> = v.iter().map(|x| x / norm).collect();
arc.write().expect("write").insert(i, &vn);
}
let query: Vec<f32> = {
let v: Vec<f32> = (0u64..8)
.map(|d| ((7 * 7 + d * 3) % 17) as f32 / 17.0)
.collect();
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-9);
v.iter().map(|x| x / norm).collect()
};
let results = arc.read().expect("read").search(&query, 10, 50);
assert!(!results.is_empty(), "must return at least 1 result");
assert_eq!(
results[0].0, 7,
"top result must be the query node itself (id=7)"
);
}
#[test]
fn set_vector_param_populates_hnsw() {
let (_dir, db) = make_db();
db.create_vector_index("Memory", "embedding", 4, "cosine")
.expect("create index");
db.execute("CREATE (n:Memory {id: 'k1'})")
.expect("CREATE node");
let emb: Vec<f32> = vec![0.1, 0.2, 0.3, 0.4];
let mut params = std::collections::HashMap::new();
params.insert("id".to_string(), Value::String("k1".to_string()));
params.insert("emb".to_string(), Value::Vector(emb.clone()));
db.execute_with_params("MATCH (n:Memory {id: $id}) SET n.embedding = $emb", params)
.expect("SET with vector param must not error");
let arc = db
.get_vector_index("Memory", "embedding")
.expect("index must exist");
let idx = arc.read().expect("read lock");
let results = idx.search(&emb, 5, 20);
assert!(
!results.is_empty(),
"vectorSearch after SET must return the inserted node (HNSW was empty — silent data loss bug)"
);
assert_eq!(results.len(), 1, "exactly one node should be in the index");
}
#[test]
fn set_vector_param_hnsw_roundtrip_survives_reopen() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().to_path_buf();
{
let db = GraphDb::open(&path).expect("open");
db.create_vector_index("Chunk", "emb", 3, "cosine")
.expect("create index");
db.execute("CREATE (n:Chunk {id: 'c1'})").expect("CREATE");
let mut params = std::collections::HashMap::new();
params.insert("id".to_string(), Value::String("c1".to_string()));
params.insert("emb".to_string(), Value::Vector(vec![1.0, 0.0, 0.0]));
db.execute_with_params("MATCH (n:Chunk {id: $id}) SET n.emb = $emb", params)
.expect("SET emb");
}
let db = GraphDb::open(&path).expect("reopen");
let arc = db
.get_vector_index("Chunk", "emb")
.expect("index must survive restart");
let idx = arc.read().expect("read");
let results = idx.search(&[1.0_f32, 0.0, 0.0], 5, 20);
assert!(
!results.is_empty(),
"node must be in HNSW after restart (persistence verification)"
);
}
#[test]
fn anonymous_match_set_vector_populates_hnsw() {
let (_dir, db) = make_db();
db.create_vector_index("Memory", "embedding", 3, "cosine")
.expect("create index");
db.execute("CREATE (n:Memory {id: 'anon-1'})")
.expect("CREATE node");
let emb = vec![1.0_f32, 0.0, 0.0];
let mut params = std::collections::HashMap::new();
params.insert("id".to_string(), Value::String("anon-1".to_string()));
params.insert("emb".to_string(), Value::Vector(emb.clone()));
db.execute_with_params("MATCH (n) WHERE n.id = $id SET n.embedding = $emb", params)
.expect("anonymous MATCH SET must succeed");
let arc = db
.get_vector_index("Memory", "embedding")
.expect("index must exist");
let results = arc.read().expect("read").search(&emb, 5, 20);
assert!(
!results.is_empty(),
"anonymous MATCH SET must populate HNSW (was silently skipped before fix)"
);
}
#[test]
fn set_vector_hnsw_label_derived_from_node_id() {
let (_dir, db) = make_db();
db.create_vector_index("PersonVec", "emb", 2, "cosine")
.expect("create PersonVec index");
db.create_vector_index("DocVec", "emb", 2, "cosine")
.expect("create DocVec index");
db.execute("CREATE (n:PersonVec {id: 'p1'})")
.expect("CREATE PersonVec node");
db.execute("CREATE (n:DocVec {id: 'd1'})")
.expect("CREATE DocVec node");
let doc_emb = vec![0.0_f32, 1.0];
let mut params = std::collections::HashMap::new();
params.insert("id".to_string(), Value::String("d1".to_string()));
params.insert("emb".to_string(), Value::Vector(doc_emb.clone()));
db.execute_with_params("MATCH (n:DocVec {id: $id}) SET n.emb = $emb", params)
.expect("SET DocVec embedding");
let doc_arc = db.get_vector_index("DocVec", "emb").expect("DocVec index");
let doc_results = doc_arc.read().expect("read").search(&doc_emb, 5, 20);
assert!(
!doc_results.is_empty(),
"DocVec HNSW must contain the SET node"
);
let person_arc = db
.get_vector_index("PersonVec", "emb")
.expect("PersonVec index");
let person_results = person_arc.read().expect("read").search(&doc_emb, 5, 20);
assert!(
person_results.is_empty(),
"PersonVec HNSW must NOT contain the DocVec node (wrong-label write)"
);
}
#[test]
fn create_vector_index_euclidean_metric() {
let (_dir, db) = make_db();
db.create_vector_index("Point", "pos", 2, "euclidean")
.expect("create euclidean index");
let arc = db.get_vector_index("Point", "pos").expect("index");
arc.write().expect("w").insert(0, &[0.0_f32, 0.0]);
arc.write().expect("w").insert(1, &[1.0_f32, 0.0]);
arc.write().expect("w").insert(2, &[0.0_f32, 10.0]);
let results = arc.read().expect("r").search(&[0.0, 0.0], 1, 10);
assert_eq!(results[0].0, 0, "nearest to origin should be origin itself");
}
#[test]
fn create_vector_index_dot_product_metric() {
let (_dir, db) = make_db();
db.create_vector_index("Emb", "feat", 2, "dot")
.expect("create dot index");
let arc = db.get_vector_index("Emb", "feat").expect("index");
arc.write().expect("w").insert(0, &[0.5_f32, 0.5]);
arc.write().expect("w").insert(1, &[1.0_f32, 1.0]);
let results = arc.read().expect("r").search(&[1.0, 1.0], 1, 10);
assert_eq!(results[0].0, 1, "highest dot product should be node 1");
}
#[test]
fn unwind_match_set_vector_param_populates_hnsw() {
let (_dir, db) = make_db();
db.create_vector_index("Memory", "embedding", 4, "cosine")
.expect("create index");
db.execute("CREATE (n:Memory {id: 'k1'})")
.expect("CREATE node");
let emb: Vec<f32> = vec![0.1, 0.2, 0.3, 0.4];
let mut params = std::collections::HashMap::new();
params.insert(
"rows".to_string(),
Value::List(vec![Value::Map(vec![(
"id".to_string(),
Value::String("k1".to_string()),
)])]),
);
params.insert("emb".to_string(), Value::Vector(emb.clone()));
db.execute_with_params(
"UNWIND $rows AS row MATCH (n:Memory {id: row.id}) SET n.embedding = $emb",
params,
)
.expect("UNWIND SET with vector param must not error");
let arc = db
.get_vector_index("Memory", "embedding")
.expect("index must exist");
let idx = arc.read().expect("read lock");
let results = idx.search(&emb, 5, 20);
assert!(
!results.is_empty(),
"vectorSearch after UNWIND MATCH SET must return the inserted node — \
an empty HNSW means the property was written and the index skipped (#410 class)"
);
assert_eq!(results.len(), 1, "exactly one node should be in the index");
}
#[test]
fn unwind_duplicate_set_same_prop_indexes_the_last_vector() {
let (_dir, db) = make_db();
db.create_vector_index("Memory", "embedding", 4, "cosine")
.expect("create index");
db.execute("CREATE (n:Memory {id: 'k1'})").expect("CREATE");
let a: Vec<f32> = vec![1.0, 0.0, 0.0, 0.0];
let b: Vec<f32> = vec![0.0, 1.0, 0.0, 0.0];
let mut params = std::collections::HashMap::new();
params.insert(
"rows".to_string(),
Value::List(vec![Value::Map(vec![(
"id".to_string(),
Value::String("k1".to_string()),
)])]),
);
params.insert("a".to_string(), Value::Vector(a.clone()));
params.insert("b".to_string(), Value::Vector(b.clone()));
db.execute_with_params(
"UNWIND $rows AS row MATCH (n:Memory {id: row.id}) SET n.embedding = $a, n.embedding = $b",
params,
)
.expect("duplicate SET on one prop must not error");
let arc = db
.get_vector_index("Memory", "embedding")
.expect("index must exist");
let idx = arc.read().expect("read lock");
let hits_b = idx.search(&b, 5, 20);
assert_eq!(hits_b.len(), 1, "one matched node => one indexed vector");
let score_b = hits_b[0].1;
let score_a = idx.search(&a, 5, 20)[0].1;
assert!(
score_b > score_a,
"index must hold the LAST vector ($b) to agree with the stored property; \
got score_b={score_b} score_a={score_a} — first-write-wins leaves the \
property as $b while the index holds $a"
);
}