#![cfg(feature = "vector-extension")]
use akar_common::types::Value;
use akar_main::{Connection, Database, SystemConfig};
use std::sync::Arc;
use std::sync::mpsc;
use std::time::Duration;
use tempfile::tempdir;
fn setup() -> (tempfile::TempDir, Arc<Database>, Connection) {
let dir = tempdir().unwrap();
let db = Arc::new(Database::new(dir.path().to_str().unwrap(), SystemConfig::default()).unwrap());
let conn = Connection::new(&db);
conn.query("CREATE NODE TABLE Memory (id INT64, content STRING, embedding FLOAT[], PRIMARY KEY (id))")
.expect("create Memory table");
(dir, db, conn)
}
#[test]
fn create_vector_index_twice_no_deadlock() {
let (_dir, _db, conn) = setup();
conn.query("CREATE VECTOR INDEX mem_vec ON (Memory.embedding) WITH (metric=cosine, dims=384)")
.expect("first create should succeed");
let (tx, rx) = mpsc::channel();
let handle = std::thread::spawn(move || {
let r = conn.query("CREATE VECTOR INDEX mem_vec ON (Memory.embedding) WITH (metric=cosine, dims=384)");
let _ = tx.send(r);
});
let result = match rx.recv_timeout(Duration::from_secs(15)) {
Ok(r) => r,
Err(_) => {
panic!("second CREATE VECTOR INDEX deadlocked on the catalog mutex");
}
};
handle.join().expect("worker thread panicked");
match result {
Err(e) => assert!(e.contains("already exists"), "expected 'already exists', got: {e}"),
Ok(_) => panic!("second CREATE VECTOR INDEX should have errored with 'already exists'"),
}
}
#[test]
fn vector_index_does_not_break_scalar_queries() {
let (_dir, _db, conn) = setup();
conn.query("CREATE VECTOR INDEX mem_vec ON (Memory.embedding) WITH (metric=cosine, dims=384)")
.expect("first create");
conn.query("CREATE VECTOR INDEX mem_vec_2 ON (Memory.embedding) WITH (metric=cosine, dims=384)")
.expect("second create on same column");
conn.query("CREATE (m:Memory {id: 1, content: 'hello', embedding: [0.1, 0.2]})")
.expect("insert row");
let res = conn
.query("MATCH (m:Memory) RETURN m.id, m.content")
.expect("scalar query");
let chunk = res.chunks.first().expect("one chunk");
assert_eq!(chunk.size, 1, "one row expected");
}
#[test]
fn complex_type_list_column_round_trips() {
let (_dir, _db, conn) = setup();
conn.query("CREATE (m:Memory {id: 1, content: 'hello', embedding: [0.1, 0.2, 0.3]})")
.expect("insert row");
let res = conn.query("MATCH (m:Memory) RETURN m.embedding").expect("list query");
let chunk = res.chunks.first().expect("one chunk");
assert_eq!(chunk.size, 1, "one row expected");
let val = chunk.get_value(0, 0).expect("embedding must not be null");
match val {
Value::List(items) => {
assert_eq!(items.len(), 3, "embedding has 3 elements, got {items:?}");
for item in &items {
assert!(
matches!(item, Value::Float(_) | Value::Double(_)),
"expected numeric embedding element, got {item:?}"
);
}
}
other => panic!("expected Value::List, got {other:?}"),
}
}
#[test]
fn complex_type_map_literal_returns_struct() {
let (_dir, _db, conn) = setup();
conn.query("CREATE (m:Memory {id: 1, content: 'hello', embedding: [0.1, 0.2]})")
.expect("insert row");
let res = conn
.query("MATCH (m:Memory) RETURN {id: m.id}")
.expect("map literal query");
let chunk = res.chunks.first().expect("one chunk");
assert_eq!(chunk.size, 1, "one row expected");
let val = chunk.get_value(0, 0).expect("map literal must not be null");
match val {
Value::Struct(entries) => {
assert_eq!(entries.len(), 1, "one field, got {entries:?}");
assert_eq!(entries[0].0, "id");
assert_eq!(entries[0].1, Value::Int64(1));
}
other => panic!("expected Value::Struct, got {other:?}"),
}
}
#[test]
fn complex_type_array_cosine_similarity_returns_double() {
let (_dir, _db, conn) = setup();
conn.query("CREATE (m:Memory {id: 1, content: 'hello', embedding: [1.0, 0.0]})")
.expect("insert row");
let res = conn
.query("MATCH (m:Memory) RETURN array_cosine_similarity(m.embedding, [1.0, 0.0])")
.expect("cosine query");
let chunk = res.chunks.first().expect("one chunk");
assert_eq!(chunk.size, 1, "one row expected");
let val = chunk.get_value(0, 0).expect("cosine similarity must not be null");
match val {
Value::Double(d) => assert!((d - 1.0).abs() < 1e-9, "expected ~1.0, got {d}"),
other => panic!("expected Value::Double, got {other:?}"),
}
}
#[test]
fn call_vector_similarity_scan_runs_hnsw() {
let (_dir, _db, conn) = setup();
conn.query("CREATE VECTOR INDEX mem_vec ON (Memory.embedding) WITH (metric=cosine, dims=2)")
.expect("create vector index");
conn.query("CREATE (m:Memory {id: 1, content: 'one', embedding: [1.0, 0.0]})")
.expect("insert one");
conn.query("CREATE (m:Memory {id: 2, content: 'two', embedding: [0.0, 1.0]})")
.expect("insert two");
let res = conn
.query("CALL vector_similarity_scan('Memory', 'embedding', [0.9, 0.1], 2)")
.expect("vector_similarity_scan must execute (previously errored)");
let chunk = res.chunks.first().expect("one chunk");
assert_eq!(chunk.size, 2, "expected 2 nearest rows, got {}", chunk.size);
assert_eq!(
chunk.field_names,
vec!["id", "content", "embedding", "distance", "_id"],
"vector-scan chunk must carry real field names (was empty)"
);
let distance_col = 3;
let id_col = 4;
let mut ids = vec![];
for row in 0..chunk.size {
let id = chunk.get_value(0, row).expect("id column present");
ids.push(id.clone());
let dist = chunk.get_value(distance_col, row).expect("distance column present");
match dist {
Value::Double(_) => {}
other => panic!("distance must be a Double, got {other:?}"),
}
let row_id = chunk.get_value(id_col, row).expect("_id column present");
match row_id {
Value::Int64(_) => {}
other => panic!("_id must be an Int64 physical row offset, got {other:?}"),
}
}
let mut got: Vec<i64> = ids
.into_iter()
.map(|v| match v {
Value::Int64(i) => i,
other => panic!("id must be Int64, got {other:?}"),
})
.collect();
got.sort_unstable();
assert_eq!(got, vec![1, 2], "both inserted rows should be returned");
}
#[test]
fn match_vector_similarity_rewrites_to_vector_scan() {
let (_dir, _db, conn) = setup();
conn.query("CREATE VECTOR INDEX mem_vec ON (Memory.embedding) WITH (metric=cosine, dims=2)")
.expect("create vector index");
conn.query("CREATE (m:Memory {id: 1, content: 'one', embedding: [1.0, 0.0]})")
.expect("insert one");
conn.query("CREATE (m:Memory {id: 2, content: 'two', embedding: [0.0, 1.0]})")
.expect("insert two");
let sql = "MATCH (m:Memory) \
WHERE cosine_similarity(m.embedding, [0.9, 0.1]) > 0.5 \
RETURN m.id \
ORDER BY cosine_similarity(m.embedding, [0.9, 0.1]) DESC \
LIMIT 2";
let res = conn.query(sql).expect("vector match query must execute");
let chunk = res.chunks.first().expect("one chunk");
assert_eq!(chunk.size, 1, "threshold 0.5 must keep only id 1, got {}", chunk.size);
match chunk.get_value(0, 0).expect("id present") {
Value::Int64(v) => assert_eq!(v, 1),
other => panic!("expected id 1, got {other:?}"),
}
let sql2 = "MATCH (m:Memory) \
WHERE cosine_similarity(m.embedding, [0.9, 0.1]) > 0.0 \
RETURN m.id \
ORDER BY cosine_similarity(m.embedding, [0.9, 0.1]) DESC \
LIMIT 2";
let res2 = conn.query(sql2).expect("vector match query 2");
let chunk2 = res2.chunks.first().expect("one chunk");
assert_eq!(chunk2.size, 2, "both rows pass threshold 0.0");
let first = match chunk2.get_value(0, 0).expect("id present") {
Value::Int64(v) => v,
other => panic!("expected Int64 id, got {other:?}"),
};
assert_eq!(first, 1, "DESC cosine order must put id 1 first (cos 0.994 > 0.110)");
}
#[test]
fn match_vector_scan_cosine_scale_invariant() {
let (_dir, _db, conn) = setup();
conn.query("CREATE VECTOR INDEX mem_vec ON (Memory.embedding) WITH (metric=cosine, dims=2)")
.expect("create vector index");
conn.query("CREATE (m:Memory {id: 1, content: 'one', embedding: [10.0, 0.0]})")
.expect("insert one");
conn.query("CREATE (m:Memory {id: 2, content: 'two', embedding: [0.0, 0.5]})")
.expect("insert two");
let sql = "MATCH (m:Memory) \
WHERE cosine_similarity(m.embedding, [0.9, 0.1]) > 0.5 \
RETURN m.id \
ORDER BY cosine_similarity(m.embedding, [0.9, 0.1]) DESC \
LIMIT 2";
let res = conn.query(sql).expect("vector match query must execute");
let chunk = res.chunks.first().expect("one chunk");
assert_eq!(
chunk.size, 1,
"scale-invariant: threshold 0.5 must keep only id 1, got {}",
chunk.size
);
match chunk.get_value(0, 0).expect("id present") {
Value::Int64(v) => assert_eq!(v, 1),
other => panic!("expected id 1, got {other:?}"),
}
let sql2 = "MATCH (m:Memory) \
WHERE cosine_similarity(m.embedding, [0.9, 0.1]) > 0.0 \
RETURN m.id \
ORDER BY cosine_similarity(m.embedding, [0.9, 0.1]) DESC \
LIMIT 2";
let res2 = conn.query(sql2).expect("vector match query 2");
let chunk2 = res2.chunks.first().expect("one chunk");
assert_eq!(chunk2.size, 2, "scale-invariant: both rows pass threshold 0.0");
let first = match chunk2.get_value(0, 0).expect("id present") {
Value::Int64(v) => v,
other => panic!("expected Int64 id, got {other:?}"),
};
assert_eq!(
first, 1,
"scale-invariant: DESC cosine order must put id 1 first (cos 0.994 > 0.110)"
);
}