use super::*;
use std::borrow::Cow;
fn cell(graph: &DirGraph, query: &str) -> Value {
let parsed = parser::parse_cypher(query)
.unwrap_or_else(|e| panic!("query failed to parse: {query}\n error: {e}"));
let no_params = HashMap::new();
let result = CypherExecutor::with_params(graph, &no_params, None)
.execute(&parsed)
.unwrap_or_else(|e| panic!("query failed: {query}\n error: {e}"));
assert_eq!(result.rows.len(), 1, "expected one row from: {query}");
assert_eq!(result.rows[0].len(), 1, "expected one column from: {query}");
result.rows[0][0].clone()
}
fn error(graph: &DirGraph, query: &str) -> String {
let parsed = parser::parse_cypher(query)
.unwrap_or_else(|e| panic!("query failed to parse: {query}\n error: {e}"));
let no_params = HashMap::new();
match CypherExecutor::with_params(graph, &no_params, None).execute(&parsed) {
Ok(result) => panic!("query unexpectedly succeeded: {query}\n rows: {result:?}"),
Err(e) => e,
}
}
fn float(graph: &DirGraph, query: &str) -> f64 {
match cell(graph, query) {
Value::Float64(f) => f,
other => panic!("expected Float64 from {query}, got {other:?}"),
}
}
#[test]
fn dot_is_the_sum_of_elementwise_products() {
let graph = DirGraph::new();
assert_eq!(float(&graph, "RETURN dot([1, 2, 3], [4, 5, 6])"), 32.0);
assert_eq!(float(&graph, "RETURN dot([0.5, 2], [4, 0.25])"), 2.5);
assert_eq!(float(&graph, "RETURN dot([1, 0], [0, 1])"), 0.0);
assert_eq!(float(&graph, "RETURN dot([1, -2], [-1, 3])"), -7.0);
assert_eq!(float(&graph, "RETURN dot([], [])"), 0.0);
assert_eq!(
float(
&graph,
"RETURN dot([1,2,3,4,5,6,7,8,9,10], [1,2,3,4,5,6,7,8,9,10])"
),
385.0
);
}
#[test]
fn norm_is_the_euclidean_length() {
let graph = DirGraph::new();
assert_eq!(float(&graph, "RETURN norm([3, 4])"), 5.0);
assert_eq!(float(&graph, "RETURN norm([1])"), 1.0);
assert_eq!(float(&graph, "RETURN norm([5, 12])"), 13.0);
assert_eq!(float(&graph, "RETURN norm([0, 0, 0])"), 0.0);
assert_eq!(float(&graph, "RETURN norm([])"), 0.0);
assert_eq!(float(&graph, "RETURN norm([-3, -4])"), 5.0);
}
#[test]
fn cosine_is_the_normalised_dot_product() {
let graph = DirGraph::new();
assert_eq!(float(&graph, "RETURN cosine([1, 2, 3], [1, 2, 3])"), 1.0);
assert_eq!(float(&graph, "RETURN cosine([1, 2, 3], [2, 4, 6])"), 1.0);
assert_eq!(float(&graph, "RETURN cosine([1, 0], [0, 1])"), 0.0);
assert_eq!(float(&graph, "RETURN cosine([1, 2], [-1, -2])"), -1.0);
let got = float(&graph, "RETURN cosine([1, 0], [1, 1])");
assert!(
(got - std::f64::consts::FRAC_1_SQRT_2).abs() < 1e-12,
"cosine([1,0],[1,1]) = {got}"
);
assert!((float(&graph, "RETURN cosine([1, 0], [3, 4])") - 0.6).abs() < 1e-12);
}
#[test]
fn a_null_argument_makes_the_whole_call_null() {
let graph = DirGraph::new();
for query in [
"RETURN dot(null, [1, 2])",
"RETURN dot([1, 2], null)",
"RETURN dot(null, null)",
"RETURN cosine(null, [1, 2])",
"RETURN cosine([1, 2], null)",
"RETURN norm(null)",
] {
assert_eq!(cell(&graph, query), Value::Null, "{query}");
}
}
#[test]
fn a_missing_property_reads_as_null_not_as_an_error() {
let graph = build_test_graph();
assert_eq!(
cell(
&graph,
"MATCH (n:Person) WHERE n.name = 'Alice' RETURN norm(n.vec)"
),
Value::Null
);
assert_eq!(
cell(
&graph,
"MATCH (n:Person) WHERE n.name = 'Alice' RETURN cosine(n.vec, [1, 2])"
),
Value::Null
);
}
#[test]
fn a_length_mismatch_is_an_error_naming_both_lengths() {
let graph = DirGraph::new();
let msg = error(&graph, "RETURN dot([1, 2], [1, 2, 3])");
assert!(
msg.contains("same length") && msg.contains('2') && msg.contains('3'),
"message must name both lengths: {msg}"
);
let msg = error(&graph, "RETURN cosine([1, 2, 3], [])");
assert!(msg.contains("same length"), "{msg}");
}
#[test]
fn a_non_numeric_element_is_an_error_naming_its_position() {
let graph = DirGraph::new();
let msg = error(&graph, "RETURN dot([1, 'x'], [1, 2])");
assert!(
msg.contains("element 1") && msg.contains("first"),
"message must name the vector and the position: {msg}"
);
let msg = error(&graph, "RETURN dot([1, 2], [1, 'x'])");
assert!(msg.contains("element 1") && msg.contains("second"), "{msg}");
let msg = error(&graph, "RETURN norm([1, true])");
assert!(msg.contains("element 1"), "{msg}");
let msg = error(&graph, "RETURN norm([1, null])");
assert!(msg.contains("element 1") && msg.contains("Null"), "{msg}");
}
#[test]
fn a_non_list_argument_is_an_error_even_opposite_a_null() {
let graph = DirGraph::new();
let msg = error(&graph, "RETURN dot(7, [1, 2])");
assert!(msg.contains("list of numbers"), "{msg}");
let msg = error(&graph, "RETURN dot(null, 7)");
assert!(msg.contains("list of numbers"), "{msg}");
let msg = error(&graph, "RETURN norm('not a list')");
assert!(msg.contains("list of numbers"), "{msg}");
}
#[test]
fn the_wrong_arity_is_an_error() {
let graph = DirGraph::new();
assert!(error(&graph, "RETURN dot([1, 2])").contains("2 arguments"));
assert!(error(&graph, "RETURN cosine([1], [1], [1])").contains("2 arguments"));
assert!(error(&graph, "RETURN norm([1], [2])").contains("1 argument"));
}
#[test]
fn cosine_of_a_zero_length_vector_is_null_not_nan() {
let graph = DirGraph::new();
for query in [
"RETURN cosine([0, 0], [1, 2])",
"RETURN cosine([1, 2], [0, 0])",
"RETURN cosine([0, 0], [0, 0])",
"RETURN cosine([], [])",
] {
assert_eq!(cell(&graph, query), Value::Null, "{query}");
}
assert_eq!(float(&graph, "RETURN dot([0, 0], [1, 2])"), 0.0);
assert_eq!(float(&graph, "RETURN norm([0, 0])"), 0.0);
}
fn docs_with_vectors() -> DirGraph {
let mut graph = DirGraph::new();
for (id, title, vec) in [
(
1u32,
"native",
Value::List(vec![Value::Float64(3.0), Value::Float64(4.0)]),
),
(
2,
"ints",
Value::List(vec![Value::Int64(1), Value::Int64(0)]),
),
(3, "text", Value::String("[3.0, 4.0]".to_string())),
] {
let node = NodeData::new(
Value::UniqueId(id),
Value::String(title.to_string()),
"Doc".to_string(),
HashMap::from([("vec".to_string(), vec)]),
&mut graph.interner,
);
let idx = graph.graph.add_node(node);
graph
.type_indices
.entry_or_default("Doc".to_string())
.push(idx);
}
graph
}
#[test]
fn the_functions_read_stored_list_properties() {
let graph = docs_with_vectors();
assert_eq!(
float(
&graph,
"MATCH (d:Doc) WHERE d.title = 'native' RETURN norm(d.vec)"
),
5.0
);
assert_eq!(
float(
&graph,
"MATCH (d:Doc) WHERE d.title = 'native' RETURN dot(d.vec, [1, 0])"
),
3.0
);
assert!(
(float(
&graph,
"MATCH (d:Doc) WHERE d.title = 'native' RETURN cosine(d.vec, [1, 0])"
) - 0.6)
.abs()
< 1e-12
);
assert_eq!(
float(
&graph,
"MATCH (d:Doc) WHERE d.title = 'ints' RETURN norm(d.vec)"
),
1.0
);
assert_eq!(
float(
&graph,
"MATCH (d:Doc) WHERE d.title = 'text' RETURN norm(d.vec)"
),
5.0
);
}
#[test]
fn two_stored_vectors_can_be_compared_against_each_other() {
let graph = docs_with_vectors();
let query = "MATCH (a:Doc) MATCH (b:Doc) \
WHERE a.title = 'native' AND b.title = 'ints' \
RETURN cosine(a.vec, b.vec)";
assert!((float(&graph, query) - 0.6).abs() < 1e-12);
}
#[test]
fn a_stored_list_reaches_the_kernel_without_being_cloned() {
use crate::graph::storage::GraphRead;
let graph = docs_with_vectors();
let idx = graph
.type_indices
.get("Doc")
.expect("Doc type index")
.get(0)
.expect("at least one Doc");
let view = graph.graph.node_view(idx).expect("node view");
let key = crate::graph::schema::InternedKey::from_str("vec");
let got = view.get(key).expect("stored vec property");
assert!(
matches!(got, Cow::Borrowed(_)),
"an in-memory list property must be borrowed, not cloned"
);
}