Skip to content

Commit e8bba83

Browse files
committed
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.
1 parent 7591d69 commit e8bba83

2 files changed

Lines changed: 93 additions & 0 deletions

File tree

crates/omnigraph-compiler/src/ir/lower_tests.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,63 @@ return { $f.name, $f.age }
3939
assert_eq!(ir.return_exprs.len(), 2);
4040
}
4141

42+
#[test]
43+
fn test_lower_undirected_traversal_to_direction_both() {
44+
let catalog = setup();
45+
let qf = parse_query(
46+
r#"
47+
query q($name: String) {
48+
match {
49+
$p: Person { name: $name }
50+
$p <knows> $f
51+
}
52+
return { $f.name }
53+
}
54+
"#,
55+
)
56+
.unwrap();
57+
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
58+
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
59+
match &ir.pipeline[1] {
60+
IROp::Expand { direction, .. } => assert_eq!(*direction, Direction::Both),
61+
op => panic!("expected Expand, got {op:?}"),
62+
}
63+
}
64+
65+
// The discarded-context-clone regression: negation inners are typechecked into
66+
// a clone that never reaches lowering's ResolvedTraversal lookup, so direction
67+
// used to silently fall back to Out inside not{}. Undirectedness now travels
68+
// on the AST node; this pins Both surviving into the AntiJoin's inner Expand.
69+
#[test]
70+
fn test_lower_undirected_inside_negation_keeps_direction_both() {
71+
let catalog = setup();
72+
let qf = parse_query(
73+
r#"
74+
query q() {
75+
match {
76+
$p: Person
77+
not { $p <knows> $_ }
78+
}
79+
return { $p.name }
80+
}
81+
"#,
82+
)
83+
.unwrap();
84+
let tc = typecheck_query(&catalog, &qf.queries[0]).unwrap();
85+
let ir = lower_query(&catalog, &qf.queries[0], &tc).unwrap();
86+
let IROp::AntiJoin { inner, .. } = &ir.pipeline[1] else {
87+
panic!("expected AntiJoin, got {:?}", ir.pipeline[1]);
88+
};
89+
match &inner[0] {
90+
IROp::Expand { direction, .. } => assert_eq!(
91+
*direction,
92+
Direction::Both,
93+
"negation inner must not fall back to Out"
94+
),
95+
op => panic!("expected inner Expand, got {op:?}"),
96+
}
97+
}
98+
4299
#[test]
43100
fn test_lower_negation() {
44101
let catalog = setup();

crates/omnigraph/tests/traversal_indexed.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,42 @@ query connected($name: String) {
141141
assert_eq!(got, vec!["Alice", "Diana"], "out ∪ in neighbors of Bob");
142142
}
143143

144+
/// The undirected cost fix (review follow-up): coverage is priced by the
145+
/// WORST of the two probed columns. This test degrades coverage via a
146+
/// mutation-appended unindexed fragment and asserts undirected results stay
147+
/// correct and mode-equivalent regardless of which path the cost model picks.
148+
#[tokio::test]
149+
async fn indexed_matches_csr_undirected_under_degraded_coverage() {
150+
let dir = tempfile::tempdir().unwrap();
151+
let mut db = init_and_load(&dir).await;
152+
153+
// Append an edge through the mutation path — an unindexed fragment that
154+
// degrades BTREE coverage on the Knows table (pinned by the coverage test
155+
// above). Diana->Alice also gives Alice a NEW incoming edge that only the
156+
// undirected form can see from Alice's side.
157+
mutate_main(
158+
&mut db,
159+
MUTATION_QUERIES,
160+
"add_friend",
161+
&params(&[("$from", "Diana"), ("$to", "Alice")]),
162+
)
163+
.await
164+
.unwrap();
165+
166+
let queries = r#"
167+
query connected($name: String) {
168+
match {
169+
$p: Person { name: $name }
170+
$p <knows> $f
171+
}
172+
return { $f.name }
173+
}
174+
"#;
175+
// Alice: outgoing Bob, Charlie; incoming Diana (the fresh unindexed edge).
176+
let got = both_modes(&mut db, queries, "connected", &params(&[("$name", "Alice")])).await;
177+
assert_eq!(got, vec!["Bob", "Charlie", "Diana"]);
178+
}
179+
144180
#[tokio::test]
145181
async fn indexed_matches_csr_multi_hop_same_type() {
146182
let dir = tempfile::tempdir().unwrap();

0 commit comments

Comments
 (0)