use std::collections::HashMap;
use super::execute::{execute_mut, execute_read, ExecuteOptions};
use crate::datatypes::Value;
use crate::graph::dir_graph::DirGraph;
use crate::graph::languages::cypher::plan_cache;
use crate::graph::languages::cypher::plan_cache::instrumentation;
fn empty_params() -> HashMap<String, Value> {
HashMap::new()
}
fn params(pairs: &[(&str, Value)]) -> HashMap<String, Value> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect()
}
fn fleet() -> DirGraph {
let p = empty_params();
let opts = ExecuteOptions::eager(&p);
let mut graph = DirGraph::new();
execute_mut(
&mut graph,
"CREATE (:Vessel {id: 1, flag: 'NO'}), (:Vessel {id: 2, flag: 'NO'}), \
(:Vessel {id: 3, flag: 'NO'}), (:Vessel {id: 4, flag: 'SE'}), \
(:Vessel {id: 5, flag: 'SE'})",
&opts,
)
.expect("seed");
execute_mut(
&mut graph,
"MATCH (a:Vessel {id: 1}), (b:Vessel {id: 4}) CREATE (a)-[:SISTER {since: 1990}]->(b)",
&opts,
)
.expect("seed edge");
graph
}
fn read_err(graph: &DirGraph, query: &str, p: &HashMap<String, Value>) -> String {
let opts = ExecuteOptions::eager(p);
execute_read(graph, query, &opts)
.err()
.unwrap_or_else(|| panic!("{query} must not succeed"))
.to_string()
}
fn row_count(graph: &DirGraph, query: &str, p: &HashMap<String, Value>) -> i64 {
let opts = ExecuteOptions::eager(p);
let outcome = execute_read(graph, query, &opts).expect("read");
match outcome.result.rows[0].first().expect("one column") {
Value::Int64(n) => *n,
other => panic!("expected a count, got {other:?}"),
}
}
#[test]
fn an_unbound_inline_map_parameter_raises() {
let graph = fleet();
let err = read_err(
&graph,
"MATCH (v:Vessel {flag: $flag}) RETURN count(v) AS c",
&empty_params(),
);
assert!(err.contains("Missing parameter: $flag"), "{err}");
}
#[test]
fn the_inline_map_and_where_spellings_report_the_same_thing() {
let graph = fleet();
for projection in ["RETURN v.id AS id", "RETURN count(v) AS c"] {
let inline = read_err(
&graph,
&format!("MATCH (v:Vessel {{flag: $flag}}) {projection}"),
&empty_params(),
);
let where_clause = read_err(
&graph,
&format!("MATCH (v:Vessel) WHERE v.flag = $flag {projection}"),
&empty_params(),
);
assert_eq!(inline, where_clause, "{projection}");
}
}
#[test]
fn a_bound_inline_map_parameter_still_matches() {
let graph = fleet();
let bound = params(&[("flag", Value::String("NO".into()))]);
assert_eq!(
row_count(
&graph,
"MATCH (v:Vessel {flag: $flag}) RETURN count(v) AS c",
&bound
),
3
);
assert_eq!(
row_count(
&graph,
"MATCH (v:Vessel) WHERE v.flag = $flag RETURN count(v) AS c",
&bound
),
3
);
let both = params(&[
("flag", Value::String("NO".into())),
("floor", Value::Int64(2)),
]);
assert_eq!(
row_count(
&graph,
"MATCH (v:Vessel {flag: $flag}) WHERE v.id >= $floor RETURN count(v) AS c",
&both
),
2
);
let err = read_err(
&graph,
"MATCH (v:Vessel {flag: $flag}) WHERE v.id >= $floor RETURN count(v) AS c",
¶ms(&[("floor", Value::Int64(2))]),
);
assert!(err.contains("Missing parameter: $flag"), "{err}");
}
#[test]
fn the_same_unbound_query_raises_on_every_call() {
let _guard = plan_cache::TEST_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
let graph = fleet();
let query = "MATCH (v:Vessel {flag: $flag}) RETURN count(v) AS c";
instrumentation::reset();
let first = read_err(&graph, query, &empty_params());
let second = read_err(&graph, query, &empty_params());
let _ = row_count(
&graph,
"MATCH (v:Vessel {flag: 'SE'}) RETURN count(v) AS c",
&empty_params(),
);
let totals = instrumentation::totals();
assert!(first.contains("Missing parameter: $flag"), "{first}");
assert_eq!(first, second, "the second call must not go quiet");
let stats = totals.unclassified;
assert_eq!(
stats.lookups, 2,
"both calls must consult the cache, else this proves nothing: {stats:?}"
);
assert_eq!(stats.hits, 0, "{stats:?}");
assert_eq!(
stats.insertions, 0,
"an erroring statement must leave nothing cached: {stats:?}"
);
}
#[test]
fn a_clean_query_still_hits_the_plan_cache() {
let _guard = plan_cache::TEST_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
let graph = fleet();
let query = "MATCH (v:Vessel {flag: 'NO'}) RETURN count(v) AS c";
instrumentation::reset();
assert_eq!(row_count(&graph, query, &empty_params()), 3);
assert_eq!(row_count(&graph, query, &empty_params()), 3);
let stats = instrumentation::totals().read;
assert_eq!(stats.hits, 1, "{stats:?}");
}
#[test]
fn every_read_pattern_position_raises_end_to_end() {
let graph = fleet();
for query in [
"MATCH (v:Vessel {flag: $flag}) RETURN v",
"MATCH (v:Vessel) OPTIONAL MATCH (w:Vessel {flag: $flag}) RETURN v, w",
"MATCH (a:Vessel)-[:SISTER {since: $flag}]->(b) RETURN a",
"MATCH (a:Vessel) WHERE EXISTS { MATCH (a)-[:SISTER]->(:Vessel {flag: $flag}) } RETURN a",
"MATCH (a:Vessel) RETURN COUNT { (a)-[:SISTER]->(:Vessel {flag: $flag}) } AS n",
"CALL { MATCH (v:Vessel {flag: $flag}) RETURN v } RETURN v",
"MATCH (v:Vessel {flag: $flag}) RETURN v UNION MATCH (v:Vessel) RETURN v",
] {
let err = read_err(&graph, query, &empty_params());
assert!(err.contains("Missing parameter: $flag"), "{query}: {err}");
}
}
#[test]
fn write_clauses_raise_through_the_evaluator() {
let p = empty_params();
let opts = ExecuteOptions::eager(&p);
for query in [
"CREATE (v:Vessel {flag: $flag})",
"MERGE (v:Vessel {flag: $flag})",
] {
let mut graph = fleet();
let err = execute_mut(&mut graph, query, &opts)
.err()
.unwrap_or_else(|| panic!("{query} must not succeed"))
.to_string();
assert!(err.contains("Missing parameter: $flag"), "{query}: {err}");
}
}
#[test]
fn an_unbound_label_wins_over_an_unbound_map_parameter() {
let graph = fleet();
let err = read_err(
&graph,
"MATCH (v:$label {flag: $flag}) RETURN v",
&empty_params(),
);
assert!(
err.contains("$label") && err.contains("label or relationship type"),
"{err}"
);
}