use std::collections::HashMap;
use super::execute::{execute_mut, execute_read, ExecuteOptions, ExecuteOutcome};
use crate::datatypes::Value;
use crate::graph::dir_graph::DirGraph;
use crate::graph::languages::cypher::result::QueryDiagnostics;
fn empty_params() -> HashMap<String, Value> {
HashMap::new()
}
fn seeded(n: i64) -> DirGraph {
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
let mut graph = DirGraph::new();
for seq in 1..=n {
execute_mut(
&mut graph,
&format!("CREATE (:Item {{id: {seq}, seq: {seq}}})"),
&opts,
)
.expect("seed write");
}
graph
}
fn read_capped(graph: &DirGraph, query: &str, row_limit: Option<usize>) -> ExecuteOutcome {
let params = empty_params();
let mut opts = ExecuteOptions::eager(¶ms);
opts.row_limit = row_limit;
execute_read(graph, query, &opts).expect("read")
}
fn diagnostics(outcome: &ExecuteOutcome) -> &QueryDiagnostics {
outcome
.result
.diagnostics
.as_ref()
.expect("every execution carries diagnostics")
}
fn seqs(outcome: &ExecuteOutcome) -> Vec<i64> {
outcome
.result
.rows
.iter()
.map(|row| match row.first() {
Some(Value::Int64(n)) => *n,
other => panic!("expected an integer seq, got {other:?}"),
})
.collect()
}
#[test]
fn cap_truncates_and_reports_the_exact_pre_truncation_total() {
let graph = seeded(50);
let outcome = read_capped(
&graph,
"MATCH (n:Item) RETURN n.seq ORDER BY n.seq",
Some(5),
);
assert_eq!(seqs(&outcome), vec![1, 2, 3, 4, 5]);
let d = diagnostics(&outcome);
assert_eq!(d.row_limit, Some(5));
assert_eq!(
d.total_rows,
Some(50),
"the total must be the exact pre-truncation count, not the retained count"
);
}
#[test]
fn under_cap_result_is_untouched_and_carries_no_truncation_signal() {
let graph = seeded(5);
let outcome = read_capped(
&graph,
"MATCH (n:Item) RETURN n.seq ORDER BY n.seq",
Some(500),
);
assert_eq!(seqs(&outcome), vec![1, 2, 3, 4, 5]);
let d = diagnostics(&outcome);
assert_eq!(d.row_limit, Some(500), "the cap in force is still echoed");
assert_eq!(
d.total_rows, None,
"nothing was dropped, so nothing to report"
);
assert!(
!d.warnings.iter().any(|w| w.contains("row_limit")),
"an untruncated result must not be warned about: {:?}",
d.warnings
);
}
#[test]
fn cap_equal_to_row_count_does_not_truncate() {
let graph = seeded(5);
let outcome = read_capped(
&graph,
"MATCH (n:Item) RETURN n.seq ORDER BY n.seq",
Some(5),
);
assert_eq!(seqs(&outcome), vec![1, 2, 3, 4, 5]);
assert_eq!(diagnostics(&outcome).total_rows, None);
}
#[test]
fn no_cap_leaves_both_fields_none() {
let graph = seeded(5);
let outcome = read_capped(&graph, "MATCH (n:Item) RETURN n.seq", None);
let d = diagnostics(&outcome);
assert_eq!(d.row_limit, None);
assert_eq!(d.total_rows, None);
}
#[test]
fn cap_of_zero_retains_nothing_and_still_counts() {
let graph = seeded(7);
let outcome = read_capped(&graph, "MATCH (n:Item) RETURN n.seq", Some(0));
assert!(outcome.result.rows.is_empty());
assert_eq!(
outcome.result.columns,
vec!["n.seq".to_string()],
"a RETURN's columns survive a zero cap — only rows are capped"
);
let d = diagnostics(&outcome);
assert_eq!(d.row_limit, Some(0));
assert_eq!(d.total_rows, Some(7));
}
#[test]
fn cap_applies_after_order_by_so_it_keeps_the_real_top_n() {
let graph = seeded(50);
let outcome = read_capped(
&graph,
"MATCH (n:Item) RETURN n.seq ORDER BY n.seq DESC",
Some(5),
);
assert_eq!(seqs(&outcome), vec![50, 49, 48, 47, 46]);
assert_eq!(diagnostics(&outcome).total_rows, Some(50));
}
#[test]
fn query_limit_and_row_limit_take_the_minimum() {
let graph = seeded(50);
let cap_wins = read_capped(
&graph,
"MATCH (n:Item) RETURN n.seq ORDER BY n.seq LIMIT 20",
Some(5),
);
assert_eq!(seqs(&cap_wins), vec![1, 2, 3, 4, 5]);
assert_eq!(
diagnostics(&cap_wins).total_rows,
Some(20),
"the total is the rows the query produced, which LIMIT had already bounded"
);
let limit_wins = read_capped(
&graph,
"MATCH (n:Item) RETURN n.seq ORDER BY n.seq LIMIT 3",
Some(5),
);
assert_eq!(seqs(&limit_wins), vec![1, 2, 3]);
assert_eq!(diagnostics(&limit_wins).total_rows, None);
}
#[test]
fn cap_applies_to_aggregation_output_rows() {
let graph = seeded(9);
let outcome = read_capped(
&graph,
"MATCH (n:Item) RETURN n.seq AS seq, count(*) AS c ORDER BY seq",
Some(4),
);
assert_eq!(outcome.result.rows.len(), 4);
let d = diagnostics(&outcome);
assert_eq!(
d.total_rows,
Some(9),
"nine groups were computed; four were kept"
);
}
#[test]
fn scalar_aggregate_survives_a_generous_cap() {
let graph = seeded(1000);
let outcome = read_capped(&graph, "MATCH (n:Item) RETURN count(*) AS c", Some(5));
assert_eq!(outcome.result.rows.len(), 1);
assert_eq!(outcome.result.rows[0][0], Value::Int64(1000));
assert_eq!(diagnostics(&outcome).total_rows, None);
}
#[test]
fn set_operations_see_their_full_arms() {
let graph = seeded(20);
let outcome = read_capped(
&graph,
"MATCH (n:Item) RETURN n.seq AS seq \
EXCEPT \
MATCH (n:Item) WHERE n.seq > 3 RETURN n.seq AS seq",
Some(2),
);
assert_eq!(
diagnostics(&outcome).total_rows,
Some(3),
"the right arm must be evaluated in full before the cap applies"
);
let mut kept = seqs(&outcome);
kept.sort_unstable();
assert_eq!(kept.len(), 2);
assert!(
kept.iter().all(|seq| (1..=3).contains(seq)),
"kept rows must come from the correct difference, got {kept:?}"
);
}
#[test]
fn union_all_counts_both_arms_before_capping() {
let graph = seeded(10);
let outcome = read_capped(
&graph,
"MATCH (n:Item) RETURN n.seq AS seq \
UNION ALL \
MATCH (n:Item) RETURN n.seq AS seq",
Some(3),
);
assert_eq!(outcome.result.rows.len(), 3);
assert_eq!(diagnostics(&outcome).total_rows, Some(20));
}
#[test]
fn subquery_bodies_are_not_capped() {
let graph = seeded(30);
let outcome = read_capped(
&graph,
"CALL { MATCH (n:Item) RETURN n.seq AS seq } RETURN count(seq) AS c",
Some(2),
);
assert_eq!(outcome.result.rows.len(), 1);
assert_eq!(
outcome.result.rows[0][0],
Value::Int64(30),
"the subquery must see all 30 rows even though the cap is 2"
);
}
#[test]
fn lazy_path_is_capped_with_an_exact_total() {
let graph = seeded(40);
let params = empty_params();
let mut opts = ExecuteOptions::eager(¶ms);
opts.lazy_eligible = true;
opts.row_limit = Some(6);
let outcome = execute_read(&graph, "MATCH (n:Item) RETURN n.seq", &opts).expect("read");
let retained = match outcome.result.lazy.as_ref() {
Some(descriptor) => descriptor.len(),
None => outcome.result.rows.len(),
};
assert_eq!(retained, 6);
let d = outcome
.result
.diagnostics
.as_ref()
.expect("diagnostics")
.clone();
assert_eq!(d.row_limit, Some(6));
assert_eq!(d.total_rows, Some(40));
}
#[test]
fn truncation_raises_a_query_warning_naming_both_counts() {
let graph = seeded(50);
let outcome = read_capped(&graph, "MATCH (n:Item) RETURN n.seq", Some(5));
let warning = diagnostics(&outcome)
.warnings
.iter()
.find(|w| w.contains("row_limit"))
.expect("a truncated result must warn");
assert!(
warning.contains('5') && warning.contains("50"),
"the warning must carry the cap and the total: {warning}"
);
}
#[test]
fn mutation_return_is_capped_while_every_write_still_lands() {
let mut graph = seeded(12);
let params = empty_params();
let mut opts = ExecuteOptions::eager(¶ms);
opts.row_limit = Some(3);
let outcome = execute_mut(
&mut graph,
"MATCH (n:Item) SET n.touched = true RETURN n.seq",
&opts,
)
.expect("mutation");
assert!(outcome.is_mutation);
assert_eq!(outcome.result.rows.len(), 3);
let d = outcome
.result
.diagnostics
.as_ref()
.expect("diagnostics")
.clone();
assert_eq!(d.total_rows, Some(12));
assert_eq!(
outcome
.result
.stats
.as_ref()
.expect("mutation stats")
.properties_set,
12,
"the cap reports fewer rows; it must never write fewer properties"
);
let check = read_capped(
&graph,
"MATCH (n:Item) WHERE n.touched = true RETURN count(*) AS c",
None,
);
assert_eq!(check.result.rows[0][0], Value::Int64(12));
}
#[test]
fn a_cap_does_not_convert_a_budget_overrun_into_a_truncation() {
let graph = seeded(50);
let params = empty_params();
let mut opts = ExecuteOptions::eager(¶ms);
opts.max_work_units = Some(2);
opts.row_limit = Some(1);
let error = match execute_read(&graph, "MATCH (n:Item) RETURN n.seq", &opts) {
Err(error) => error,
Ok(_) => panic!("the work budget must still fail the query"),
};
assert!(
error.to_string().contains("max_work_units"),
"unexpected error: {error}"
);
}
#[test]
fn explain_is_exempt_from_the_cap() {
let graph = seeded(50);
let uncapped = read_capped(&graph, "EXPLAIN MATCH (n:Item) RETURN n.seq", None);
let capped = read_capped(&graph, "EXPLAIN MATCH (n:Item) RETURN n.seq", Some(1));
assert!(capped.explain);
assert_eq!(capped.result.rows.len(), uncapped.result.rows.len());
assert_eq!(diagnostics(&capped).total_rows, None);
}