mod test_helpers;
use std::collections::BTreeMap;
use lora_database::LoraValue;
use lora_store::{LoraVector, RawCoordinate, VectorCoordinateType};
use serde_json::json;
use test_helpers::TestDb;
#[test]
fn vector_integer_construction() {
let v = TestDb::new().scalar("RETURN [1, 2, 3]::VECTOR<INTEGER>(3) AS v");
assert_eq!(v["kind"], "vector");
assert_eq!(v["dimension"], 3);
assert_eq!(v["coordinateType"], "INTEGER");
assert_eq!(v["values"], json!([1, 2, 3]));
}
#[test]
fn vector_float_construction() {
let v = TestDb::new().scalar("RETURN [1.05, 0.123, 5]::VECTOR<FLOAT32>(3) AS v");
assert_eq!(v["coordinateType"], "FLOAT32");
assert_eq!(v["dimension"], 3);
let arr = v["values"].as_array().unwrap();
assert!((arr[0].as_f64().unwrap() - 1.05).abs() < 1e-5);
assert!((arr[1].as_f64().unwrap() - 0.123).abs() < 1e-5);
assert!((arr[2].as_f64().unwrap() - 5.0).abs() < 1e-5);
}
#[test]
fn vector_from_string_with_scientific_notation() {
let v = TestDb::new().scalar("RETURN '[1.05e+00, 0.123, 5]'::VECTOR<FLOAT>(3) AS v");
assert_eq!(v["coordinateType"], "FLOAT64");
let arr = v["values"].as_array().unwrap();
assert!((arr[0].as_f64().unwrap() - 1.05).abs() < 1e-9);
}
#[test]
fn vector_with_string_coordinate_type() {
let v = TestDb::new().scalar("RETURN cast.to([1, 2, 3], 'VECTOR<INTEGER8>(3)') AS v");
assert_eq!(v["coordinateType"], "INTEGER8");
assert_eq!(v["values"], json!([1, 2, 3]));
}
#[test]
fn vector_accepts_signed_integer_alias_as_string() {
let v = TestDb::new().scalar("RETURN cast.to([10], 'VECTOR<SIGNED INTEGER>(1)') AS v");
assert_eq!(v["coordinateType"], "INTEGER");
}
#[test]
fn vector_from_parameter_list() {
let mut params = BTreeMap::new();
params.insert(
"values".into(),
LoraValue::List(vec![
LoraValue::Int(1),
LoraValue::Int(2),
LoraValue::Int(3),
LoraValue::Int(4),
LoraValue::Int(5),
]),
);
let rows = TestDb::new().run_with_params("RETURN $values::VECTOR<INTEGER8>(5) AS v", params);
assert_eq!(rows[0]["v"]["dimension"], 5);
assert_eq!(rows[0]["v"]["coordinateType"], "INTEGER8");
}
#[test]
fn vector_value_type_reports_vector() {
let v = TestDb::new().scalar("RETURN type.of([1,2,3]::VECTOR<INTEGER>(3)) AS t");
assert_eq!(v.as_str().unwrap(), "VECTOR<INTEGER>(3)");
}
#[test]
fn vector_dimension_zero_errors() {
let err = TestDb::new().run_err("RETURN []::VECTOR<INTEGER>(0) AS v");
assert!(err.contains("dimension"), "got: {err}");
}
#[test]
fn vector_dimension_over_max_errors() {
let err = TestDb::new().run_err("RETURN [1]::VECTOR<INTEGER>(5000) AS v");
assert!(err.contains("dimension"), "got: {err}");
}
#[test]
fn vector_dimension_mismatch_errors() {
let err = TestDb::new().run_err("RETURN [1,2,3]::VECTOR<INTEGER>(2) AS v");
assert!(
err.contains("dimension") || err.contains("cannot cast"),
"got: {err}"
);
}
#[test]
fn vector_int8_overflow_errors() {
let err = TestDb::new().run_err("RETURN [128]::VECTOR<INT8>(1) AS v");
assert!(
err.contains("range") || err.contains("INTEGER8"),
"got: {err}"
);
}
#[test]
fn vector_float_to_int_truncates() {
let v = TestDb::new().scalar("RETURN [1.2, -2.9]::VECTOR<INT>(2) AS v");
assert_eq!(v["values"], json!([1, -2]));
}
#[test]
fn vector_int_to_float_is_allowed() {
let v = TestDb::new().scalar("RETURN [3, 4]::VECTOR<FLOAT32>(2) AS v");
let arr = v["values"].as_array().unwrap();
assert_eq!(arr[0].as_f64().unwrap(), 3.0);
assert_eq!(arr[1].as_f64().unwrap(), 4.0);
}
#[test]
fn vector_nested_list_errors() {
let err = TestDb::new().run_err("RETURN [[1,2]]::VECTOR<INTEGER>(1) AS v");
assert!(err.contains("nested") || err.contains("list"), "got: {err}");
}
#[test]
fn vector_unknown_coordinate_type_errors() {
let err = TestDb::new().run_err("RETURN cast.to([1], 'VECTOR<FLOAT128>(1)') AS v");
assert!(
err.contains("coordinate type") || err.contains("unknown cast target type"),
"got: {err}"
);
}
#[test]
fn vector_null_value_returns_null() {
let v = TestDb::new().scalar("RETURN null::VECTOR<FLOAT32>(3) AS v");
assert!(v.is_null());
}
#[test]
fn vector_null_dimension_returns_null() {
let v = TestDb::new().scalar("RETURN cast.try([1,2,3], 'VECTOR<INTEGER8>(null)') AS v");
assert!(v.is_null());
}
#[test]
fn vector_on_node_property_persists_and_returns() {
let db = TestDb::new();
db.run("CREATE (:Doc {id: 1, embedding: [1,2,3]::VECTOR<INTEGER>(3)})");
let v = db.scalar("MATCH (d:Doc {id: 1}) RETURN d.embedding AS e");
assert_eq!(v["kind"], "vector");
assert_eq!(v["coordinateType"], "INTEGER");
assert_eq!(v["values"], json!([1, 2, 3]));
}
#[test]
fn vector_on_relationship_property_persists() {
let db = TestDb::new();
db.run("CREATE (:A {id: 1}), (:A {id: 2})");
db.run(
"MATCH (a:A {id:1}), (b:A {id:2}) \
CREATE (a)-[:SIM {score: [0.1,0.2]::VECTOR<FLOAT32>(2)}]->(b)",
);
let v = db.scalar("MATCH (:A)-[r:SIM]->(:A) RETURN r.score AS s");
assert_eq!(v["dimension"], 2);
assert_eq!(v["coordinateType"], "FLOAT32");
}
#[test]
fn vector_set_updates_property() {
let db = TestDb::new();
db.run("CREATE (:Doc {id: 1, embedding: [0,0]::VECTOR<INTEGER>(2)})");
db.run("MATCH (d:Doc {id:1}) SET d.embedding = [0.1, 0.2]::VECTOR<FLOAT32>(2)");
let v = db.scalar("MATCH (d:Doc {id:1}) RETURN d.embedding AS e");
assert_eq!(v["dimension"], 2);
assert_eq!(v["coordinateType"], "FLOAT32");
}
#[test]
fn vector_nested_in_list_property_rejected() {
let db = TestDb::new();
let err = db.run_err(
"CREATE (:Doc {embeddings: [[1,2,3]::VECTOR<INTEGER>(3), [4,5,6]::VECTOR<INTEGER>(3)]})",
);
assert!(
err.contains("VECTOR") || err.contains("vector"),
"got: {err}"
);
}
#[test]
fn coordinates_as_integer_roundtrip() {
let v = TestDb::new()
.scalar("RETURN vector.coordinates([1.9, -1.9, 3]::VECTOR<FLOAT32>(3), INTEGER) AS l");
assert_eq!(v, json!([1, -1, 3]));
}
#[test]
fn coordinates_as_float_roundtrip() {
let v =
TestDb::new().scalar("RETURN vector.coordinates([1, 2, 3]::VECTOR<INT8>(3), FLOAT) AS l");
let arr = v.as_array().unwrap();
assert_eq!(arr.len(), 3);
assert_eq!(arr[0].as_f64().unwrap(), 1.0);
}
#[test]
fn vector_dimension_count_matches_dimension() {
let v = TestDb::new().scalar("RETURN vector.dim([1, 2, 3]::VECTOR<INTEGER8>(3)) AS size");
assert_eq!(v, json!(3));
}
#[test]
fn size_of_vector_equals_dimension() {
let v = TestDb::new().scalar("RETURN value.size([1, 2, 3, 4]::VECTOR<FLOAT32>(4)) AS s");
assert_eq!(v, json!(4));
}
#[test]
fn vector_equality() {
let v = TestDb::new()
.scalar("RETURN [1,2,3]::VECTOR<INTEGER>(3) = [1,2,3]::VECTOR<INTEGER>(3) AS eq");
assert_eq!(v, json!(true));
}
#[test]
fn vector_distinct_collapses_duplicates() {
let db = TestDb::new();
db.run("CREATE (:V {id: 1, e: [1,2]::VECTOR<INT>(2)})");
db.run("CREATE (:V {id: 2, e: [1,2]::VECTOR<INT>(2)})");
db.run("CREATE (:V {id: 3, e: [1,3]::VECTOR<INT>(2)})");
let rows = db.run("MATCH (v:V) RETURN DISTINCT v.e AS e");
assert_eq!(rows.len(), 2);
}
#[test]
fn cosine_similarity_identical_vectors() {
let v = TestDb::new().scalar(
"RETURN vector.similarity([1,0,0]::VECTOR<FLOAT32>(3), [1,0,0]::VECTOR<FLOAT32>(3)) AS s",
);
assert!((v.as_f64().unwrap() - 1.0).abs() < 1e-6);
}
#[test]
fn cosine_similarity_accepts_list_inputs() {
let v = TestDb::new().scalar("RETURN vector.similarity([1,0,0], [0,1,0]) AS s");
assert!((v.as_f64().unwrap() - 0.5).abs() < 1e-6);
}
#[test]
fn cosine_similarity_on_zero_vector_is_null() {
let v = TestDb::new().scalar(
"RETURN vector.similarity([0,0,0]::VECTOR<FLOAT32>(3), [1,0,0]::VECTOR<FLOAT32>(3)) AS s",
);
assert!(v.is_null());
}
#[test]
fn euclidean_similarity_matches_documented_example() {
let v = TestDb::new().scalar(
"RETURN vector.similarity([4.0,5.0,6.0], [2.0,8.0,3.0]::VECTOR<FLOAT32>(3), 'euclidean') AS s",
);
let sim = v.as_f64().unwrap();
assert!((sim - 1.0 / 23.0).abs() < 1e-6, "got {sim}");
}
#[test]
fn vector_distance_euclidean() {
let v = TestDb::new().scalar(
"RETURN vector.distance([1.0, 5.0, 3.0, 6.7]::VECTOR<FLOAT32>(4), \
[5.0, 2.5, 3.1, 9.0]::VECTOR<FLOAT32>(4), EUCLIDEAN) AS d",
);
let d = v.as_f64().unwrap();
assert!((d - 5.2488).abs() < 1e-3, "got {d}");
}
#[test]
fn vector_distance_euclidean_squared() {
let v = TestDb::new().scalar(
"RETURN vector.distance([1,0,0]::VECTOR<INTEGER8>(3), \
[0,1,0]::VECTOR<INTEGER8>(3), EUCLIDEAN_SQUARED) AS d",
);
assert!((v.as_f64().unwrap() - 2.0).abs() < 1e-6);
}
#[test]
fn vector_distance_manhattan() {
let v = TestDb::new().scalar(
"RETURN vector.distance([1,2,3]::VECTOR<INTEGER>(3), \
[4,0,1]::VECTOR<INTEGER>(3), MANHATTAN) AS d",
);
assert!((v.as_f64().unwrap() - 7.0).abs() < 1e-6);
}
#[test]
fn vector_distance_cosine() {
let v = TestDb::new().scalar(
"RETURN vector.distance([1,2,3]::VECTOR<INTEGER8>(3), \
[1,2,3]::VECTOR<INTEGER8>(3), COSINE) AS d",
);
assert!((v.as_f64().unwrap() - 0.0).abs() < 1e-5);
}
#[test]
fn vector_distance_dot() {
let v = TestDb::new().scalar(
"RETURN vector.distance([1,2,3]::VECTOR<INTEGER>(3), \
[4,5,6]::VECTOR<INTEGER>(3), DOT) AS d",
);
assert!((v.as_f64().unwrap() - (-32.0)).abs() < 1e-5);
}
#[test]
fn vector_distance_hamming() {
let v = TestDb::new().scalar(
"RETURN vector.distance([1,2,3,4]::VECTOR<INTEGER8>(4), \
[1,0,3,0]::VECTOR<INTEGER8>(4), HAMMING) AS d",
);
assert!((v.as_f64().unwrap() - 2.0).abs() < 1e-6);
}
#[test]
fn vector_distance_requires_matching_dimensions() {
let err = TestDb::new().run_err(
"RETURN vector.distance([1,2]::VECTOR<INTEGER>(2), \
[1,2,3]::VECTOR<INTEGER>(3), EUCLIDEAN) AS d",
);
assert!(err.contains("dimension"), "got: {err}");
}
#[test]
fn vector_norm_euclidean() {
let v = TestDb::new()
.scalar("RETURN vector.norm([1.0, 5.0, 3.0, 6.7]::VECTOR<FLOAT32>(4), EUCLIDEAN) AS n");
let n = v.as_f64().unwrap();
assert!((n - 8.938).abs() < 1e-3, "got {n}");
}
#[test]
fn vector_norm_manhattan() {
let v = TestDb::new()
.scalar("RETURN vector.norm([1.0, -5.0, 3.0, -6.7]::VECTOR<FLOAT32>(4), MANHATTAN) AS n");
assert!((v.as_f64().unwrap() - 15.7).abs() < 1e-3);
}
#[test]
fn exhaustive_knn_ranking() {
let db = TestDb::new();
db.run("CREATE (:Doc {id: 1, embedding: [1.0, 0.0, 0.0]::VECTOR<FLOAT32>(3)})");
db.run("CREATE (:Doc {id: 2, embedding: [0.9, 0.1, 0.0]::VECTOR<FLOAT32>(3)})");
db.run("CREATE (:Doc {id: 3, embedding: [0.0, 1.0, 0.0]::VECTOR<FLOAT32>(3)})");
db.run("CREATE (:Doc {id: 4, embedding: [-1.0, 0.0, 0.0]::VECTOR<FLOAT32>(3)})");
let mut params = BTreeMap::new();
params.insert(
"query".into(),
LoraValue::Vector(
LoraVector::try_new(
vec![
RawCoordinate::Float(1.0),
RawCoordinate::Float(0.0),
RawCoordinate::Float(0.0),
],
3,
VectorCoordinateType::Float32,
)
.unwrap(),
),
);
let rows = db.run_with_params(
"MATCH (d:Doc) \
RETURN d.id AS id \
ORDER BY vector.similarity(d.embedding, $query) DESC LIMIT 3",
params,
);
assert_eq!(rows.len(), 3);
assert_eq!(rows[0]["id"], 1);
assert_eq!(rows[1]["id"], 2);
}
#[test]
fn exhaustive_knn_with_euclidean_similarity() {
let db = TestDb::new();
db.run("CREATE (:Node {id: 1, vec: [4.0, 5.0, 6.0]::VECTOR<FLOAT32>(3)})");
db.run("CREATE (:Node {id: 2, vec: [2.0, 8.0, 3.0]::VECTOR<FLOAT32>(3)})");
db.run("CREATE (:Node {id: 3, vec: [10.0, 10.0, 10.0]::VECTOR<FLOAT32>(3)})");
let mut params = BTreeMap::new();
params.insert(
"query".into(),
LoraValue::List(vec![
LoraValue::Float(4.0),
LoraValue::Float(5.0),
LoraValue::Float(6.0),
]),
);
let rows = db.run_with_params(
"MATCH (n:Node) \
WITH n, vector.similarity($query, n.vec) AS score \
RETURN n.id AS id, score \
ORDER BY score DESC LIMIT 2",
params,
);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["id"], 1);
assert!(rows[0]["score"].as_f64().unwrap() > rows[1]["score"].as_f64().unwrap());
}
#[test]
fn vector_int8_construction_happy_path() {
let v = TestDb::new().scalar("RETURN [-128, 0, 127]::VECTOR<INT8>(3) AS v");
assert_eq!(v["coordinateType"], "INTEGER8");
assert_eq!(v["values"], json!([-128, 0, 127]));
}
#[test]
fn vector_string_with_nan_errors() {
let err = TestDb::new().run_err("RETURN '[1.0, NaN, 3.0]'::VECTOR<FLOAT32>(3) AS v");
assert!(
err.contains("NaN")
|| err.contains("finite")
|| err.contains("numeric")
|| err.contains("cannot cast"),
"got: {err}"
);
}
#[test]
fn vector_string_with_infinity_errors() {
let err = TestDb::new().run_err("RETURN '[1.0, Infinity, 3.0]'::VECTOR<FLOAT32>(3) AS v");
assert!(
err.contains("Infinity")
|| err.contains("finite")
|| err.contains("numeric")
|| err.contains("cannot cast"),
"got: {err}"
);
}
#[test]
fn vector_non_numeric_coordinate_errors() {
let err = TestDb::new().run_err("RETURN [1, 'two', 3]::VECTOR<INTEGER>(3) AS v");
assert!(
err.contains("numeric") || err.contains("string") || err.contains("cannot cast"),
"got: {err}"
);
}
#[test]
fn vector_rejects_double_alias() {
let err = TestDb::new().run_err("RETURN cast.to([1.0, 2.0], 'VECTOR<DOUBLE>(2)') AS v");
assert!(
err.contains("coordinate type") || err.contains("unknown cast target type"),
"got: {err}"
);
}
#[test]
fn vector_nested_in_map_list_property_rejected() {
let db = TestDb::new();
let err = db.run_err("CREATE (:Doc {meta: {embeddings: [[1,2,3]::VECTOR<INTEGER>(3)]}})");
assert!(
err.contains("VECTOR") || err.contains("vector"),
"got: {err}"
);
}
#[test]
fn map_containing_vector_directly_is_allowed_as_property() {
let db = TestDb::new();
db.run("CREATE (:Doc {meta: {embedding: [1,2,3]::VECTOR<INTEGER>(3)}})");
let v = db.scalar("MATCH (d:Doc) RETURN d.meta AS m");
let embedding = &v["embedding"];
assert_eq!(embedding["kind"], "vector");
assert_eq!(embedding["dimension"], 3);
}
#[test]
fn cosine_similarity_orthogonal_vectors_is_one_half() {
let v = TestDb::new().scalar(
"RETURN vector.similarity([1,0,0]::VECTOR<FLOAT32>(3), [0,1,0]::VECTOR<FLOAT32>(3)) AS s",
);
assert!((v.as_f64().unwrap() - 0.5).abs() < 1e-6);
}
#[test]
fn cosine_similarity_opposite_vectors_is_zero() {
let v = TestDb::new().scalar(
"RETURN vector.similarity([1,0,0]::VECTOR<FLOAT32>(3), [-1,0,0]::VECTOR<FLOAT32>(3)) AS s",
);
assert!(v.as_f64().unwrap().abs() < 1e-6);
}
#[test]
fn cosine_similarity_null_input_returns_null() {
let v =
TestDb::new().scalar("RETURN vector.similarity(null, [1,0,0]::VECTOR<FLOAT32>(3)) AS s");
assert!(v.is_null());
}
#[test]
fn euclidean_similarity_null_input_returns_null() {
let v =
TestDb::new().scalar("RETURN vector.similarity([1,0,0]::VECTOR<FLOAT32>(3), null) AS s");
assert!(v.is_null());
}
#[test]
fn vector_distance_accepts_string_metric() {
let v = TestDb::new().scalar(
"RETURN vector.distance([1,0,0]::VECTOR<INTEGER8>(3), [0,1,0]::VECTOR<INTEGER8>(3), 'EUCLIDEAN_SQUARED') AS d",
);
assert!((v.as_f64().unwrap() - 2.0).abs() < 1e-6);
}
#[test]
fn vector_distance_rejects_list_input() {
let err = TestDb::new()
.run_err("RETURN vector.distance([1,2,3], [1,2,3]::VECTOR<INTEGER>(3), EUCLIDEAN) AS d");
assert!(err.contains("VECTOR"), "got: {err}");
}
#[test]
fn vector_distance_unknown_metric_errors() {
let err = TestDb::new().run_err(
"RETURN vector.distance([1,2,3]::VECTOR<INTEGER>(3), [1,2,3]::VECTOR<INTEGER>(3), BOGUS) AS d",
);
assert!(err.contains("metric"), "got: {err}");
}
#[test]
fn vector_distance_null_input_returns_null() {
let v = TestDb::new()
.scalar("RETURN vector.distance(null, [1,2,3]::VECTOR<INTEGER>(3), EUCLIDEAN) AS d");
assert!(v.is_null());
}
#[test]
fn vector_norm_unknown_metric_errors() {
let err = TestDb::new().run_err("RETURN vector.norm([1,2,3]::VECTOR<FLOAT32>(3), COSINE) AS n");
assert!(err.contains("metric"), "got: {err}");
}
#[test]
fn vector_norm_unknown_metric_errors_in_filter() {
let err = TestDb::new().run_err(
"WITH [1,2,3]::VECTOR<FLOAT32>(3) AS v \
WHERE vector.norm(v, COSINE) > 0 \
RETURN v",
);
assert!(err.contains("metric"), "got: {err}");
}
#[test]
fn vector_norm_unknown_metric_errors_in_aggregate_argument() {
let err = TestDb::new().run_err(
"UNWIND [1] AS x \
RETURN sum(vector.norm([1,2,3]::VECTOR<FLOAT32>(3), COSINE)) AS n",
);
assert!(err.contains("metric"), "got: {err}");
}
#[test]
fn vector_norm_accepts_string_metric() {
let v = TestDb::new()
.scalar("RETURN vector.norm([3.0, 4.0]::VECTOR<FLOAT32>(2), 'EUCLIDEAN') AS n");
assert!((v.as_f64().unwrap() - 5.0).abs() < 1e-4);
}
#[test]
fn coordinates_rejects_non_vector() {
let err = TestDb::new().run_err("RETURN vector.coordinates([1, 2, 3], INTEGER) AS l");
assert!(err.contains("VECTOR"), "got: {err}");
}
#[test]
fn coordinates_as_float_null_returns_null() {
let v = TestDb::new().scalar("RETURN vector.coordinates(null, FLOAT) AS l");
assert!(v.is_null());
}
#[test]
fn vector_dimension_count_rejects_non_vector() {
let err = TestDb::new().run_err("RETURN vector.dim([1, 2, 3]) AS n");
assert!(err.contains("VECTOR"), "got: {err}");
}
#[test]
fn every_coordinate_type_round_trips_via_vector_function() {
let db = TestDb::new();
let cases: &[(&str, &str)] = &[
("FLOAT", "FLOAT64"),
("FLOAT64", "FLOAT64"),
("FLOAT32", "FLOAT32"),
("INTEGER", "INTEGER"),
("INT", "INTEGER"),
("INT64", "INTEGER"),
("INTEGER64", "INTEGER"),
("INTEGER32", "INTEGER32"),
("INT32", "INTEGER32"),
("INTEGER16", "INTEGER16"),
("INT16", "INTEGER16"),
("INTEGER8", "INTEGER8"),
("INT8", "INTEGER8"),
];
for (alias, canonical) in cases {
let q = format!("RETURN [1, 2, 3]::VECTOR<{alias}>(3) AS v");
let v = db.scalar(&q);
assert_eq!(
v["coordinateType"], *canonical,
"alias {alias} should resolve to {canonical}, got {v}"
);
assert_eq!(v["dimension"], 3);
}
}
#[test]
fn string_coordinate_aliases_are_case_and_whitespace_tolerant() {
let db = TestDb::new();
for alias in [
"integer",
" INTEGER ",
"signed integer",
"SIGNED INTEGER",
"Integer64",
] {
let q = format!("RETURN cast.to([1], 'VECTOR<{alias}>(1)') AS v");
let v = db.scalar(&q);
assert_eq!(v["coordinateType"], "INTEGER", "alias {alias:?}");
}
}
#[test]
fn vector_negative_dimension_errors() {
let err = TestDb::new().run_err("RETURN cast.to([1], 'VECTOR<INTEGER>(-1)') AS v");
assert!(
err.contains("dimension") || err.contains("unknown cast target type"),
"got: {err}"
);
}
#[test]
fn vector_non_integer_dimension_errors() {
let err = TestDb::new().run_err("RETURN cast.to([1], 'VECTOR<INTEGER>(1.5)') AS v");
assert!(
err.contains("dimension") || err.contains("unknown cast target type"),
"got: {err}"
);
}
#[test]
fn vector_null_coordinate_type_errors() {
let err = TestDb::new().run_err("RETURN cast.to([1], 'VECTOR<null>(1)') AS v");
assert!(
err.contains("coordinateType")
|| err.contains("coordinate type")
|| err.contains("unknown cast target type"),
"got: {err}"
);
}
#[test]
fn vector_value_of_wrong_type_errors() {
let db = TestDb::new();
let cases = [
("RETURN true::VECTOR<INTEGER>(1) AS v", "LIST"),
("RETURN {x: 1}::VECTOR<INTEGER>(1) AS v", "LIST"),
(
"RETURN [1]::VECTOR<INTEGER>(1)::VECTOR<FLOAT32>(1) AS v",
"VECTOR",
),
];
for (q, needle) in cases {
let err = db.run_err(q);
assert!(
err.contains(needle) || err.contains("STRING") || err.contains("cannot cast"),
"query {q:?} got: {err}"
);
}
}
#[test]
fn list_literal_of_vectors_is_allowed_as_query_value() {
let db = TestDb::new();
let v = db.scalar("RETURN [[1]::VECTOR<INTEGER>(1), [2]::VECTOR<INTEGER>(1)] AS vectors");
let arr = v.as_array().expect("array");
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["kind"], "vector");
assert_eq!(arr[1]["values"], json!([2]));
}
#[test]
fn collect_over_vector_properties_returns_list_of_vectors() {
let db = TestDb::new();
db.run("CREATE (:Doc {id: 1, embedding: [1,2,3]::VECTOR<INTEGER>(3)})");
db.run("CREATE (:Doc {id: 2, embedding: [4,5,6]::VECTOR<INTEGER>(3)})");
let rows = db.run("MATCH (d:Doc) RETURN collect(d.embedding) AS embs");
let embs = rows[0]["embs"].as_array().unwrap();
assert_eq!(embs.len(), 2);
for e in embs {
assert_eq!(e["kind"], "vector");
assert_eq!(e["dimension"], 3);
}
}
#[test]
fn unwind_vector_list_yields_vectors() {
let db = TestDb::new();
let rows = db.run(
"UNWIND [[1]::VECTOR<INTEGER>(1), [2]::VECTOR<INTEGER>(1)] AS v \
RETURN v",
);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["v"]["kind"], "vector");
assert_eq!(rows[1]["v"]["values"], json!([2]));
}
#[test]
fn bulk_insert_via_unwind_of_parameter_list_of_maps() {
let db = TestDb::new();
let make_vec = |vals: &[i64]| {
LoraValue::Vector(
LoraVector::try_new(
vals.iter().map(|v| RawCoordinate::Int(*v)).collect(),
vals.len() as i64,
VectorCoordinateType::Integer8,
)
.unwrap(),
)
};
let mut row1 = BTreeMap::new();
row1.insert("id".into(), LoraValue::Int(1));
row1.insert("title".into(), LoraValue::String("Onboarding".into()));
row1.insert("embedding".into(), make_vec(&[1, 2, 3]));
let mut row2 = BTreeMap::new();
row2.insert("id".into(), LoraValue::Int(2));
row2.insert("title".into(), LoraValue::String("Runbook".into()));
row2.insert("embedding".into(), make_vec(&[4, 5, 6]));
let mut params = BTreeMap::new();
params.insert(
"batch".into(),
LoraValue::List(vec![LoraValue::Map(row1), LoraValue::Map(row2)]),
);
db.run_with_params(
"UNWIND $batch AS row \
CREATE (:Doc {id: row.id, title: row.title, embedding: row.embedding})",
params,
);
let rows = db.run("MATCH (d:Doc) RETURN d.id AS id, d.embedding AS e ORDER BY id");
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["e"]["coordinateType"], "INTEGER8");
assert_eq!(rows[0]["e"]["values"], json!([1, 2, 3]));
assert_eq!(rows[1]["e"]["values"], json!([4, 5, 6]));
}
#[test]
fn set_plus_equals_with_vector_is_stored() {
let db = TestDb::new();
db.run("CREATE (:Doc {id: 1})");
db.run("MATCH (d:Doc {id: 1}) SET d += {embedding: [1,2]::VECTOR<FLOAT32>(2)}");
let v = db.scalar("MATCH (d:Doc {id: 1}) RETURN d.embedding AS e");
assert_eq!(v["coordinateType"], "FLOAT32");
assert_eq!(v["dimension"], 2);
}
#[test]
fn set_replace_with_vector_map_is_stored() {
let db = TestDb::new();
db.run("CREATE (:Doc {id: 1, old: 'stale'})");
db.run("MATCH (d:Doc {id: 1}) SET d = {id: 1, embedding: [0.1, 0.2]::VECTOR<FLOAT64>(2)}");
let v = db.scalar("MATCH (d:Doc {id: 1}) RETURN d.embedding AS e");
assert_eq!(v["coordinateType"], "FLOAT64");
let props = db.scalar("MATCH (d:Doc {id: 1}) RETURN value.properties(d) AS p");
assert!(props.get("old").is_none(), "old should be gone: {props}");
assert!(props.get("embedding").is_some());
}
#[test]
fn set_relationship_property_with_vector() {
let db = TestDb::new();
db.run("CREATE (:A {id: 1})-[:R]->(:A {id: 2})");
db.run(
"MATCH (:A {id: 1})-[r:R]->(:A {id: 2}) \
SET r.score = [0.9, 0.1]::VECTOR<FLOAT32>(2)",
);
let v = db.scalar("MATCH ()-[r:R]->() RETURN r.score AS s");
assert_eq!(v["coordinateType"], "FLOAT32");
}
#[test]
fn vector_parameter_stored_as_node_property() {
let db = TestDb::new();
let mut params = BTreeMap::new();
params.insert(
"embedding".into(),
LoraValue::Vector(
LoraVector::try_new(
vec![RawCoordinate::Float(0.5), RawCoordinate::Float(0.25)],
2,
VectorCoordinateType::Float32,
)
.unwrap(),
),
);
db.run_with_params("CREATE (:Doc {id: 1, embedding: $embedding})", params);
let v = db.scalar("MATCH (d:Doc {id: 1}) RETURN d.embedding AS e");
assert_eq!(v["coordinateType"], "FLOAT32");
assert_eq!(v["dimension"], 2);
}
#[test]
fn map_parameter_with_vector_value_is_stored() {
let db = TestDb::new();
let vec_val = LoraValue::Vector(
LoraVector::try_new(
vec![RawCoordinate::Int(1), RawCoordinate::Int(2)],
2,
VectorCoordinateType::Integer8,
)
.unwrap(),
);
let mut inner = BTreeMap::new();
inner.insert("embedding".to_string(), vec_val);
let mut params = BTreeMap::new();
params.insert("meta".into(), LoraValue::Map(inner));
db.run_with_params("CREATE (:Doc {id: 1, meta: $meta})", params);
let m = db.scalar("MATCH (d:Doc {id: 1}) RETURN d.meta AS m");
assert_eq!(m["embedding"]["coordinateType"], "INTEGER8");
}
#[test]
fn list_parameter_containing_vector_is_rejected_on_write() {
let db = TestDb::new();
let vec_val = LoraValue::Vector(
LoraVector::try_new(
vec![RawCoordinate::Int(1)],
1,
VectorCoordinateType::Integer8,
)
.unwrap(),
);
let mut params = BTreeMap::new();
params.insert("list".into(), LoraValue::List(vec![vec_val]));
let res = db
.service
.execute_with_params(
"CREATE (:Doc {id: 1, embeddings: $list})",
Some(lora_database::ExecuteOptions {
format: lora_database::ResultFormat::Rows,
}),
params,
)
.expect_err("should reject list-of-vectors property");
let msg = res.to_string();
assert!(
msg.contains("VECTOR") || msg.contains("vector"),
"got: {msg}"
);
}
#[test]
fn vectors_with_same_values_but_different_coord_types_are_not_equal() {
let v = TestDb::new()
.scalar("RETURN [1,2,3]::VECTOR<INTEGER>(3) = [1,2,3]::VECTOR<INTEGER8>(3) AS eq");
assert_eq!(v, json!(false));
}
#[test]
fn vectors_with_different_dimension_are_not_equal() {
let v = TestDb::new()
.scalar("RETURN [1,2]::VECTOR<INTEGER>(2) = [1,2,3]::VECTOR<INTEGER>(3) AS eq");
assert_eq!(v, json!(false));
}
#[test]
fn where_equals_matches_stored_vector() {
let db = TestDb::new();
db.run("CREATE (:Doc {id: 1, e: [1,2,3]::VECTOR<INTEGER>(3)})");
db.run("CREATE (:Doc {id: 2, e: [4,5,6]::VECTOR<INTEGER>(3)})");
let rows = db.run("MATCH (d:Doc) WHERE d.e = [1,2,3]::VECTOR<INTEGER>(3) RETURN d.id AS id");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["id"], 1);
}
#[test]
fn where_not_equals_on_stored_vectors() {
let db = TestDb::new();
db.run("CREATE (:Doc {id: 1, e: [1,2]::VECTOR<INTEGER>(2)})");
db.run("CREATE (:Doc {id: 2, e: [3,4]::VECTOR<INTEGER>(2)})");
let mut ids: Vec<i64> = db
.run("MATCH (d:Doc) WHERE d.e <> [1,2]::VECTOR<INTEGER>(2) RETURN d.id AS id")
.iter()
.map(|r| r["id"].as_i64().unwrap())
.collect();
ids.sort();
assert_eq!(ids, vec![2]);
}
#[test]
fn distinct_does_not_collapse_different_coord_types() {
let db = TestDb::new();
db.run("CREATE (:V {e: [1,2]::VECTOR<INTEGER>(2)})");
db.run("CREATE (:V {e: [1,2]::VECTOR<INTEGER8>(2)})");
let rows = db.run("MATCH (v:V) RETURN DISTINCT v.e AS e");
assert_eq!(rows.len(), 2);
}
#[test]
fn order_by_on_vector_column_is_stable_and_does_not_panic() {
let db = TestDb::new();
db.run("CREATE (:V {id: 1, e: [1,2]::VECTOR<INTEGER>(2)})");
db.run("CREATE (:V {id: 2, e: [3,4]::VECTOR<INTEGER>(2)})");
db.run("CREATE (:V {id: 3, e: [1,2]::VECTOR<INTEGER>(2)})");
let rows = db.run("MATCH (v:V) RETURN v.id AS id ORDER BY v.e");
assert_eq!(rows.len(), 3);
}
#[test]
fn coordinates_as_integer_null_returns_null() {
let v = TestDb::new().scalar("RETURN vector.coordinates(null, INTEGER) AS l");
assert!(v.is_null());
}
#[test]
fn vector_dimension_count_null_returns_null() {
let v = TestDb::new().scalar("RETURN vector.dim(null) AS n");
assert!(v.is_null());
}
#[test]
fn size_null_returns_null() {
let v = TestDb::new().scalar("RETURN value.size(null) AS s");
assert!(v.is_null());
}
#[test]
fn vector_norm_null_input_returns_null() {
let v = TestDb::new().scalar("RETURN vector.norm(null, EUCLIDEAN) AS n");
assert!(v.is_null());
}
#[test]
fn vector_norm_null_metric_returns_null() {
let v = TestDb::new().scalar("RETURN vector.norm([1,2,3]::VECTOR<FLOAT32>(3), null) AS n");
assert!(v.is_null());
}
#[test]
fn vector_distance_null_in_second_slot_returns_null() {
let v = TestDb::new()
.scalar("RETURN vector.distance([1,2,3]::VECTOR<INTEGER>(3), null, EUCLIDEAN) AS d");
assert!(v.is_null());
}
#[test]
fn vector_distance_null_metric_returns_null() {
let v = TestDb::new().scalar(
"RETURN vector.distance([1,2,3]::VECTOR<INTEGER>(3), [1,2,3]::VECTOR<INTEGER>(3), null) AS d",
);
assert!(v.is_null());
}
#[test]
fn vector_distance_metric_of_wrong_type_errors() {
let db = TestDb::new();
for bad in ["1", "[1,2]", "{k: 1}", "true"] {
let q = format!(
"RETURN vector.distance([1,2]::VECTOR<INTEGER>(2), [1,2]::VECTOR<INTEGER>(2), {bad}) AS d"
);
let err = db.run_err(&q);
assert!(err.contains("metric"), "query {q:?} got: {err}");
}
}
#[test]
fn vector_norm_metric_is_case_insensitive() {
let v = TestDb::new()
.scalar("RETURN vector.norm([3.0, 4.0]::VECTOR<FLOAT32>(2), 'euclidean') AS n");
assert!((v.as_f64().unwrap() - 5.0).abs() < 1e-4);
}
#[test]
fn vector_distance_metric_is_case_insensitive() {
let v = TestDb::new().scalar(
"RETURN vector.distance([1,0,0]::VECTOR<INTEGER>(3), [0,1,0]::VECTOR<INTEGER>(3), 'euclidean_squared') AS d",
);
assert!((v.as_f64().unwrap() - 2.0).abs() < 1e-6);
}
#[test]
fn similarity_rejects_empty_list() {
let err = TestDb::new().run_err("RETURN vector.similarity([], []) AS s");
assert!(
err.contains("empty") || err.contains("dimension") || err.contains("numeric"),
"got: {err}"
);
}
#[test]
fn similarity_rejects_non_numeric_list_entries() {
let db = TestDb::new();
for bad in [
"vector.similarity([1, 'two', 3], [1, 2, 3])",
"vector.similarity([1, null, 3], [1, 2, 3])",
"vector.similarity([1, true, 3], [1, 2, 3])",
"vector.similarity([1, [2], 3], [1, 2, 3])",
"vector.similarity([1, {x: 1}, 3], [1, 2, 3])",
] {
let q = format!("RETURN {bad} AS s");
let err = db.run_err(&q);
assert!(
err.contains("numeric")
|| err.contains("nested")
|| err.contains("null")
|| err.contains("string"),
"query {q:?} got: {err}"
);
}
}
#[test]
fn similarity_mixed_vector_and_list_input() {
let db = TestDb::new();
db.run("CREATE (:Doc {e: [1.0, 0.0, 0.0]::VECTOR<FLOAT32>(3)})");
let v = db.scalar("MATCH (d:Doc) RETURN vector.similarity(d.e, [1.0, 0.0, 0.0]) AS s");
assert!((v.as_f64().unwrap() - 1.0).abs() < 1e-6);
}
#[test]
fn vector_distance_mixed_sign_floats() {
let db = TestDb::new();
let manhattan = db.scalar(
"RETURN vector.distance([1.5, -2.5, 0.5]::VECTOR<FLOAT32>(3), [-0.5, 1.5, -1.5]::VECTOR<FLOAT32>(3), MANHATTAN) AS d",
);
assert!((manhattan.as_f64().unwrap() - 8.0).abs() < 1e-4);
let squared = db.scalar(
"RETURN vector.distance([1.5, -2.5, 0.5]::VECTOR<FLOAT32>(3), [-0.5, 1.5, -1.5]::VECTOR<FLOAT32>(3), EUCLIDEAN_SQUARED) AS d",
);
assert!((squared.as_f64().unwrap() - 24.0).abs() < 1e-3);
}
#[test]
fn vector_distance_hamming_on_float_vectors() {
let v = TestDb::new().scalar(
"RETURN vector.distance([1.0, 2.0, 3.0]::VECTOR<FLOAT32>(3), [1.0, 2.5, 3.5]::VECTOR<FLOAT32>(3), HAMMING) AS d",
);
assert!((v.as_f64().unwrap() - 2.0).abs() < 1e-9);
}
#[test]
fn vector_norm_on_integer_vector() {
let db = TestDb::new();
let l2 = db.scalar("RETURN vector.norm([3, 4]::VECTOR<INTEGER>(2), EUCLIDEAN) AS n");
assert!((l2.as_f64().unwrap() - 5.0).abs() < 1e-4);
let l1 = db.scalar("RETURN vector.norm([3, 4]::VECTOR<INTEGER>(2), MANHATTAN) AS n");
assert!((l1.as_f64().unwrap() - 7.0).abs() < 1e-4);
}