use super::*;
use crate::graph::session::execute::{execute_mut, execute_read, ExecuteOptions};
fn run(graph: &mut DirGraph, query: &str) {
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
execute_mut(graph, query, &opts).unwrap_or_else(|e| panic!("setup failed: {query}: {e}"));
}
fn fan_in_graph(hubs: i64, fan: i64) -> DirGraph {
let mut graph = DirGraph::new();
run(
&mut graph,
&format!("UNWIND range(1, {hubs}) AS b CREATE (:B {{bid: b}})"),
);
run(
&mut graph,
&format!("MATCH (x:B) UNWIND range(1, {fan}) AS i CREATE (a:A {{aid: i}})-[:R1]->(x)"),
);
run(
&mut graph,
&format!("MATCH (x:B) UNWIND range(1, {fan}) AS i CREATE (c:C {{cid: i}})-[:R2]->(x)"),
);
graph
}
fn read(graph: &DirGraph, query: &str) -> Result<usize, String> {
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
execute_read(graph, query, &opts)
.map(|outcome| outcome.result.rows.len())
.map_err(|e| e.to_string())
}
fn assert_hook_fired_and_query_is_otherwise_fine(graph: &DirGraph, query: &str, err: String) {
assert!(
err.contains("test hook"),
"expected the periodic-poll hook to fire, got: {err}"
);
assert!(
read(graph, query).is_ok(),
"non-vacuity: {query} must succeed with no hook armed"
);
}
#[test]
fn first_match_row_loop_polls_the_interrupt() {
let graph = fan_in_graph(2, 4);
let query = "MATCH (n0:A)-[r1:R1]->(n1:B)<-[r2:R2]-(n2:C) RETURN n0.aid";
CypherExecutor::interrupt_after_periodic_polls(0);
let err = read(&graph, query).expect_err("the row loop must reach a poll");
assert_hook_fired_and_query_is_otherwise_fine(&graph, query, err);
}
#[test]
fn comma_pattern_join_polls_the_interrupt() {
let graph = fan_in_graph(2, 4);
let query = "MATCH (a:A), (b:B) RETURN a.aid, b.bid";
CypherExecutor::interrupt_after_periodic_polls(1);
let err = read(&graph, query).expect_err("the comma-pattern join must reach a poll");
assert_hook_fired_and_query_is_otherwise_fine(&graph, query, err);
}
#[test]
fn subsequent_match_join_polls_the_interrupt() {
let graph = fan_in_graph(2, 4);
let query = "MATCH (a:A) WITH a MATCH (a)-[:R1]->(b:B) RETURN b.bid";
CypherExecutor::interrupt_after_periodic_polls(1);
let err = read(&graph, query).expect_err("the driving-row join must reach a poll");
assert_hook_fired_and_query_is_otherwise_fine(&graph, query, err);
}
#[test]
fn path_binding_loop_polls_the_interrupt() {
let graph = fan_in_graph(2, 4);
let query = "MATCH p = (n0:A)-[r1:R1]->(n1:B) RETURN length(p)";
CypherExecutor::interrupt_after_periodic_polls(1);
let err = read(&graph, query).expect_err("the path-binding loop must reach a poll");
assert_hook_fired_and_query_is_otherwise_fine(&graph, query, err);
}
#[test]
fn a_deadline_inside_the_row_loop_aborts_without_finishing_it() {
let graph = fan_in_graph(60, 100);
let query = "MATCH (n0:A)-[r1:R1]->(n1:B)<-[r2:R2]-(n2:C) \
WHERE n0.aid + n1.bid + n2.cid > -1 AND toString(n0.aid) <> 'zzz' \
RETURN n0.aid";
let params = HashMap::new();
let started = std::time::Instant::now();
let rows = read(&graph, query).expect("uncapped run succeeds");
let uncapped = started.elapsed();
assert_eq!(
rows, 600_000,
"the fixture must produce the row count it sizes for"
);
let mut opts = ExecuteOptions::eager(¶ms);
opts.deadline = Some(std::time::Instant::now() + uncapped / 4);
let started = std::time::Instant::now();
let err = match execute_read(&graph, query, &opts) {
Err(e) => e.to_string(),
Ok(_) => panic!("the deadline must fire"),
};
let elapsed = started.elapsed();
assert!(err.contains("timed out"), "unexpected error: {err}");
assert!(
elapsed < uncapped / 2,
"deadline at {:?} of a {uncapped:?} query aborted only after {elapsed:?} — \
the row loop ran past it",
uncapped / 4
);
}