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;
use crate::graph::schema::{NodeSchemaDefinition, SchemaDefinition, SchemaInstall};
fn empty_params() -> HashMap<String, Value> {
HashMap::new()
}
fn seeded() -> DirGraph {
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
let mut graph = DirGraph::new();
execute_mut(
&mut graph,
"CREATE (:Vessel {id: 1})-[:OPERATED_BY]->(:Operator {id: 2})",
&opts,
)
.expect("seed write");
graph
}
fn warnings_of(graph: &DirGraph, query: &str) -> Vec<String> {
warnings_with(graph, query, &empty_params())
}
fn warnings_with(graph: &DirGraph, query: &str, params: &HashMap<String, Value>) -> Vec<String> {
let opts = ExecuteOptions::eager(params);
let outcome = execute_read(graph, query, &opts).expect("read");
outcome
.result
.diagnostics
.expect("every execution carries diagnostics")
.warnings
}
#[test]
fn unknown_label_reaches_diagnostics() {
let graph = seeded();
let warnings = warnings_of(&graph, "MATCH (n:vessel) RETURN count(n) AS c");
assert!(
warnings
.iter()
.any(|w| w.contains("unknown node label 'vessel'") && w.contains("Did you mean")),
"case-typo label must reach diagnostics: {warnings:?}"
);
}
#[test]
fn unknown_relationship_reaches_diagnostics() {
let graph = seeded();
let warnings = warnings_of(&graph, "MATCH (a:Vessel)-[:OPERATED_BYY]->(b) RETURN a");
assert!(
warnings
.iter()
.any(|w| w.contains("unknown relationship type 'OPERATED_BYY'")),
"{warnings:?}"
);
}
#[test]
fn a_clean_query_carries_diagnostics_with_no_warnings() {
let graph = seeded();
let warnings = warnings_of(&graph, "MATCH (n:Vessel) RETURN count(n) AS c");
assert!(warnings.is_empty(), "{warnings:?}");
}
#[test]
fn a_plan_cache_hit_still_carries_its_warnings() {
let _guard = plan_cache::TEST_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
let graph = seeded();
let query = "MATCH (n:vessel) RETURN count(n) AS c";
instrumentation::reset();
let first = warnings_of(&graph, query);
let second = warnings_of(&graph, query);
let stats = instrumentation::totals().read;
assert_eq!(
stats.hits, 1,
"the second run must be a cache hit, else this case proves nothing: {stats:?}"
);
assert!(!first.is_empty(), "first run: {first:?}");
assert_eq!(
first, second,
"a cache hit must carry the same warnings as the miss that filled it"
);
}
#[test]
fn a_mutations_read_pattern_reaches_diagnostics() {
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
let mut graph = seeded();
let outcome =
execute_mut(&mut graph, "MATCH (n:vessel) SET n.flag = true", &opts).expect("mut");
let warnings = outcome
.result
.diagnostics
.expect("a mutation carries diagnostics too")
.warnings;
assert!(
warnings
.iter()
.any(|w| w.contains("unknown node label 'vessel'")),
"{warnings:?}"
);
let created = execute_mut(&mut graph, "CREATE (:BrandNewType {id: 9})", &opts).expect("mut");
assert!(
created
.result
.diagnostics
.expect("diagnostics")
.warnings
.is_empty(),
"a CREATE of an unseen type is not a typo"
);
}
#[test]
fn procedure_scope_warnings_reach_diagnostics() {
let graph = seeded();
let warnings = warnings_of(
&graph,
"CALL pagerank({relationship: 'OPERATED_BYY'}) YIELD node RETURN count(*) AS c",
);
assert!(
warnings
.iter()
.any(|w| w.contains("unknown relationship type 'OPERATED_BYY'")),
"{warnings:?}"
);
}
#[test]
fn explain_carries_warnings() {
let graph = seeded();
let warnings = warnings_of(&graph, "EXPLAIN MATCH (n:vessel) RETURN n");
assert!(
warnings
.iter()
.any(|w| w.contains("unknown node label 'vessel'")),
"{warnings:?}"
);
}
#[test]
fn a_subquery_bodys_procedure_warning_is_absorbed() {
let graph = seeded();
let warnings = warnings_of(
&graph,
"CALL { CALL pagerank({relationship: 'OPERATED_BYY'}) YIELD node \
RETURN count(*) AS c } RETURN c",
);
assert!(
warnings
.iter()
.any(|w| w.contains("unknown relationship type 'OPERATED_BYY'")),
"{warnings:?}"
);
}
#[test]
fn a_correlated_subquerys_warning_is_absorbed_once() {
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
let mut graph = seeded();
execute_mut(&mut graph, "CREATE (:Vessel {id: 3})", &opts).expect("second vessel");
let warnings = warnings_of(
&graph,
"MATCH (v:Vessel) CALL { WITH v CALL pagerank({relationship: 'OPERATED_BYY'}) \
YIELD node RETURN count(*) AS c } RETURN v.id, c",
);
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(
warnings[0].contains("unknown relationship type 'OPERATED_BYY'"),
"{warnings:?}"
);
}
#[test]
fn a_declared_type_mismatch_reaches_diagnostics_and_is_never_cached() {
let _guard = plan_cache::TEST_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
let mut graph = DirGraph::new();
execute_mut(&mut graph, "CREATE (:Person {id: 1, age: 30})", &opts).expect("seed");
execute_mut(
&mut graph,
"CREATE CONSTRAINT FOR (p:Person) REQUIRE p.age IS :: INTEGER",
&opts,
)
.expect("declare the property type");
let query = "MATCH (p:Person) WHERE p.age > 'forty' RETURN p";
instrumentation::reset();
let first = warnings_of(&graph, query);
let second = warnings_of(&graph, query);
let stats = instrumentation::totals().read;
assert_eq!(
(stats.insertions, stats.hits),
(0, 0),
"a promotable finding must keep the statement out of the cache: {stats:?}"
);
assert_eq!(first.len(), 1, "{first:?}");
assert!(
first[0].contains("Person.age (declared INTEGER)")
&& first[0].contains("STRING literal 'forty'")
&& first[0].contains("filters out every row"),
"{first:?}"
);
assert_eq!(
first, second,
"the second, uncached run must recompute the same warning"
);
assert!(
warnings_of(&graph, "MATCH (p:Person) WHERE p.id > 'forty' RETURN p").is_empty(),
"a built-in field carries no declaration"
);
}
#[test]
fn a_schema_defined_type_mismatch_still_rides_the_cache() {
let _guard = plan_cache::TEST_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
let mut graph = DirGraph::new();
execute_mut(&mut graph, "CREATE (:Person {id: 1, age: 30})", &opts).expect("seed");
let mut node = NodeSchemaDefinition::default();
node.field_types
.insert("age".to_string(), "integer".to_string());
let mut schema = SchemaDefinition::default();
schema.node_schemas.insert("Person".to_string(), node);
graph
.set_schema(schema, SchemaInstall::Replace)
.expect("the stored ages honour the declaration");
let query = "MATCH (p:Person) WHERE p.age > 'forty' RETURN p";
instrumentation::reset();
let first = warnings_of(&graph, query);
let second = warnings_of(&graph, query);
let stats = instrumentation::totals().read;
assert_eq!(
(stats.insertions, stats.hits),
(1, 1),
"an unpromotable finding must not cost the statement its cache entry: {stats:?}"
);
assert_eq!(first.len(), 1, "{first:?}");
assert!(
first[0].contains("Person.age (schema-defined integer)"),
"{first:?}"
);
assert_eq!(first, second, "the hit must carry the same warning");
}
#[test]
fn a_parameter_typed_mismatch_is_diagnosed_and_never_cached() {
let _guard = plan_cache::TEST_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
let empty = empty_params();
let opts = ExecuteOptions::eager(&empty);
let mut graph = DirGraph::new();
execute_mut(&mut graph, "CREATE (:Person {id: 1, age: 30})", &opts).expect("seed");
execute_mut(
&mut graph,
"CREATE CONSTRAINT FOR (p:Person) REQUIRE p.age IS :: INTEGER",
&opts,
)
.expect("declare the property type");
let query = "MATCH (p:Person) WHERE p.age > $cutoff RETURN p";
let mut bound = HashMap::new();
bound.insert("cutoff".to_string(), Value::String("forty".to_string()));
instrumentation::reset();
let first = warnings_with(&graph, query, &bound);
assert_eq!(first.len(), 1, "{first:?}");
assert!(
first[0].contains("Person.age (declared INTEGER)")
&& first[0].contains("STRING parameter $cutoff ('forty')"),
"{first:?}"
);
let mut typed = HashMap::new();
typed.insert("cutoff".to_string(), Value::Int64(40));
assert!(warnings_with(&graph, query, &typed).is_empty());
assert!(warnings_with(&graph, query, &bound).len() == 1);
let stats = instrumentation::totals().read;
assert_eq!(
stats.hits, 0,
"a parameterized statement is never cached, so nothing can be served stale: {stats:?}"
);
}