test: lowering-boundary pins for Both + undirected under degraded coverage

Closes two coverage gaps from review of my own test surface: (1) the
lowering boundary — undirected lowers to Expand{direction: Both} both
top-level and inside not{} (the discarded-context-clone regression is
now pinned at the layer it lives at, not only end-to-end); (2) the
review-fix path — undirected both_modes equivalence with a
mutation-appended unindexed fragment degrading BTREE coverage, so
results stay correct and mode-equivalent whichever path the pessimistic
coverage pricing picks.
This commit is contained in:
aaltshuler 2026-07-06 00:16:04 +03:00 committed by Andrew Altshuler
parent a41d6be88f
commit 2faa8dfa3e
2 changed files with 93 additions and 0 deletions

View file

@ -39,6 +39,63 @@ return { $f.name, $f.age }
assert_eq!(ir.return_exprs.len(), 2);
}
#[test]
fn test_lower_undirected_traversal_to_direction_both() {
let catalog = setup();
let qf = parse_query(
r#"
query q($name: String) {
match {
$p: Person { name: $name }
$p <knows> $f
}
return { $f.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
match &ir.pipeline[1] {
IROp::Expand { direction, .. } => assert_eq!(*direction, Direction::Both),
op => panic!("expected Expand, got {op:?}"),
}
}
// The discarded-context-clone regression: negation inners are typechecked into
// a clone that never reaches lowering's ResolvedTraversal lookup, so direction
// used to silently fall back to Out inside not{}. Undirectedness now travels
// on the AST node; this pins Both surviving into the AntiJoin's inner Expand.
#[test]
fn test_lower_undirected_inside_negation_keeps_direction_both() {
let catalog = setup();
let qf = parse_query(
r#"
query q() {
match {
$p: Person
not { $p <knows> $_ }
}
return { $p.name }
}
"#,
)
.unwrap();
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
let IROp::AntiJoin { inner, .. } = &ir.pipeline[1] else {
panic!("expected AntiJoin, got {:?}", ir.pipeline[1]);
};
match &inner[0] {
IROp::Expand { direction, .. } => assert_eq!(
*direction,
Direction::Both,
"negation inner must not fall back to Out"
),
op => panic!("expected inner Expand, got {op:?}"),
}
}
#[test]
fn test_lower_negation() {
let catalog = setup();

View file

@ -141,6 +141,42 @@ query connected($name: String) {
assert_eq!(got, vec!["Alice", "Diana"], "out in neighbors of Bob");
}
/// The undirected cost fix (review follow-up): coverage is priced by the
/// WORST of the two probed columns. This test degrades coverage via a
/// mutation-appended unindexed fragment and asserts undirected results stay
/// correct and mode-equivalent regardless of which path the cost model picks.
#[tokio::test]
async fn indexed_matches_csr_undirected_under_degraded_coverage() {
let dir = tempfile::tempdir().unwrap();
let mut db = init_and_load(&dir).await;
// Append an edge through the mutation path — an unindexed fragment that
// degrades BTREE coverage on the Knows table (pinned by the coverage test
// above). Diana->Alice also gives Alice a NEW incoming edge that only the
// undirected form can see from Alice's side.
mutate_main(
&mut db,
MUTATION_QUERIES,
"add_friend",
&params(&[("$from", "Diana"), ("$to", "Alice")]),
)
.await
.unwrap();
let queries = r#"
query connected($name: String) {
match {
$p: Person { name: $name }
$p <knows> $f
}
return { $f.name }
}
"#;
// Alice: outgoing Bob, Charlie; incoming Diana (the fresh unindexed edge).
let got = both_modes(&mut db, queries, "connected", &params(&[("$name", "Alice")])).await;
assert_eq!(got, vec!["Bob", "Charlie", "Diana"]);
}
#[tokio::test]
async fn indexed_matches_csr_multi_hop_same_type() {
let dir = tempfile::tempdir().unwrap();