Skip to content

Commit 5da2692

Browse files
committed
fix: Track C empty branch detection in UNION assembly
- Add is_empty_or_filtered_branch() helper to detect both explicit (LogicalPlan::Empty) and implicit (GraphRel{labels: None}) empty branches - Update UNION assembly to filter out empty branches before creating UNION - Add safety guards in analyzer phases (projection_tagging, graph_context, etc.) to skip processing when labels are None - Fixes Neo4j Browser property key queries that filter to 0 relationship types Resolves incomplete Track C implementation from PR #67 Tested: UNION with one empty branch now works correctly
1 parent 139d9eb commit 5da2692

5 files changed

Lines changed: 153 additions & 10 deletions

File tree

investigate_empty_branch.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Bug #2 Investigation: Trace where empty branch error occurs
4+
"""
5+
import requests
6+
7+
# Minimal test case: UNION with empty relationship branch
8+
query = """
9+
USE social_benchmark
10+
MATCH (n) WHERE n.country IS NOT NULL
11+
RETURN n.country
12+
UNION ALL
13+
MATCH ()-[r]-() WHERE r.country IS NOT NULL
14+
RETURN r.country
15+
"""
16+
17+
print("="*80)
18+
print("Bug #2: Empty Branch Investigation")
19+
print("="*80)
20+
print("\nQuery:")
21+
print(query)
22+
print("\nExpected: Return nodes with country, skip relationships (none have country)")
23+
print("Actual: Will error with 'Relationship type '' not found'")
24+
print("\nSending query...")
25+
26+
r = requests.post("http://localhost:8080/query", json={"query": query})
27+
print(f"\nStatus: {r.status_code}")
28+
print(f"Response: {r.text[:500]}")
29+

src/query_planner/analyzer/graph_join/inference.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3561,6 +3561,22 @@ impl GraphJoinInference {
35613561
right_is_referenced
35623562
);
35633563

3564+
// Track C: Check if relationship has types (may be filtered to 0 by property-based pruning)
3565+
// If no types, skip join inference - the Empty plan handles this case
3566+
if let Ok(rel_ctx) = plan_ctx.get_rel_table_ctx(&graph_rel.alias) {
3567+
if rel_ctx
3568+
.get_labels()
3569+
.map_or(true, |labels| labels.is_empty())
3570+
{
3571+
log::info!(
3572+
"🔧 GraphJoinInference: Skipping for relationship '{}' with no types (filtered by Track C)",
3573+
graph_rel.alias
3574+
);
3575+
crate::debug_print!(" +- infer_graph_join EXIT (empty relationship)\n");
3576+
return Ok(());
3577+
}
3578+
}
3579+
35643580
// Extract all necessary data from graph_context BEFORE passing plan_ctx mutably
35653581
let (
35663582
left_alias_str,

src/query_planner/analyzer/graph_traversal_planning.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,21 @@ impl GraphTRaversalPlanning {
362362
}
363363
}
364364

365+
// Track C: Check if relationship has types (may be filtered to 0 by property-based pruning)
366+
// If no types, skip graph traversal planning - the Empty plan handles this case
367+
if let Ok(rel_ctx) = plan_ctx.get_rel_table_ctx(&graph_rel.alias) {
368+
if rel_ctx
369+
.get_labels()
370+
.map_or(true, |labels| labels.is_empty())
371+
{
372+
log::info!(
373+
"🔧 GraphTraversalPlanning: Skipping for relationship '{}' with no types (filtered by Track C)",
374+
graph_rel.alias
375+
);
376+
return Ok((graph_rel.clone(), vec![]));
377+
}
378+
}
379+
365380
let graph_context = graph_context::get_graph_context(
366381
graph_rel,
367382
plan_ctx,

src/query_planner/analyzer/projection_tagging.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,24 @@ impl ProjectionTagging {
574574
}
575575

576576
// Get label for property resolution
577-
let label = table_ctx.get_label_opt().unwrap_or_default();
577+
let label = match table_ctx.get_label_opt() {
578+
Some(l) => l,
579+
None => {
580+
// No label - untyped pattern filtered to 0 types by Track C
581+
// Skip property resolution - the query will return 0 rows
582+
log::info!(
583+
"🔧 ProjectionTagging: Skipping property resolution for untyped pattern '{}' with no matching types (filtered to 0 by Track C)",
584+
property_access.table_alias.0
585+
);
586+
// Return property as-is - the Empty plan will handle it
587+
let projection_item = ProjectionItem {
588+
expression: item.expression.clone(),
589+
col_alias: item.col_alias.clone(),
590+
};
591+
table_ctx.insert_projection(projection_item);
592+
return Ok(());
593+
}
594+
};
578595
let is_relation = table_ctx.is_relation();
579596

580597
// Resolve property to actual column name using ViewResolver

src/query_planner/logical_plan/mod.rs

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,43 @@ pub fn evaluate_query(
125125
)
126126
}
127127

128+
/// Helper function to detect empty or filtered UNION branches
129+
///
130+
/// Track C filters branches to 0 types, but creates different "empty" representations:
131+
/// - Explicit: LogicalPlan::Empty (for nodes filtered to 0 types)
132+
/// - Implicit: GraphRel{labels: None} (for relationships filtered to 0 types)
133+
///
134+
/// This function detects both forms and recursively checks wrapped plans.
135+
fn is_empty_or_filtered_branch(plan: &LogicalPlan) -> bool {
136+
match plan {
137+
// Explicit empty
138+
LogicalPlan::Empty => true,
139+
140+
// Implicit empty: relationship filtered to 0 types by Track C
141+
LogicalPlan::GraphRel(rel) if rel.labels.is_none() => {
142+
log::debug!(
143+
"Detected filtered GraphRel (labels=None) for alias '{}'",
144+
rel.alias
145+
);
146+
true
147+
}
148+
149+
// Check if wrapped plan contains Empty
150+
LogicalPlan::GraphNode(node) => {
151+
matches!(node.input.as_ref(), LogicalPlan::Empty)
152+
}
153+
154+
// Recursively check wrapped plans (common UNION branch structures)
155+
LogicalPlan::Projection(proj) => is_empty_or_filtered_branch(&proj.input),
156+
LogicalPlan::Filter(f) => is_empty_or_filtered_branch(&f.input),
157+
LogicalPlan::GraphJoins(joins) => is_empty_or_filtered_branch(&joins.input),
158+
LogicalPlan::Limit(limit) => is_empty_or_filtered_branch(&limit.input),
159+
160+
// Not empty
161+
_ => false,
162+
}
163+
}
164+
128165
/// Evaluate a complete Cypher statement which may contain UNION clauses
129166
pub fn evaluate_cypher_statement(
130167
statement: CypherStatement<'_>,
@@ -192,18 +229,47 @@ pub fn evaluate_cypher_statement(
192229
view_parameter_values.clone(),
193230
max_inferred_types,
194231
)?;
195-
all_plans.push(plan);
196-
// Merge the context from this union branch into combined context
197-
if let Some(ref mut combined) = combined_ctx {
198-
combined.merge(ctx);
232+
233+
// Track C Property Optimization: Skip empty branches
234+
// When Track C filters a branch to 0 matching types, detect and skip it
235+
// This handles both explicit Empty and implicit GraphRel{labels: None}
236+
if !is_empty_or_filtered_branch(plan.as_ref()) {
237+
all_plans.push(plan);
238+
// Merge the context from this union branch into combined context
239+
if let Some(ref mut combined) = combined_ctx {
240+
combined.merge(ctx);
241+
}
242+
} else {
243+
log::info!(
244+
"🔀 UNION branch filtered to 0 types by Track C - skipping empty branch"
245+
);
199246
}
200247
}
201248

202-
// Create Union logical plan
203-
let union_plan = Arc::new(LogicalPlan::Union(Union {
204-
inputs: all_plans,
205-
union_type,
206-
}));
249+
// Handle different scenarios based on non-empty branch count
250+
let union_plan = match all_plans.len() {
251+
0 => {
252+
// All branches filtered to 0 types - return empty result
253+
log::info!("🔀 All UNION branches empty - returning Empty plan (0 rows)");
254+
Arc::new(LogicalPlan::Empty)
255+
}
256+
1 => {
257+
// Only one branch has data - no UNION needed
258+
log::info!("🔀 Only 1 non-empty UNION branch - skipping UNION wrapper");
259+
all_plans.into_iter().next().unwrap()
260+
}
261+
_ => {
262+
// Multiple branches with data - create UNION
263+
log::info!(
264+
"🔀 Creating UNION with {} non-empty branches",
265+
all_plans.len()
266+
);
267+
Arc::new(LogicalPlan::Union(Union {
268+
inputs: all_plans,
269+
union_type,
270+
}))
271+
}
272+
};
207273

208274
let final_ctx = combined_ctx.ok_or_else(|| {
209275
LogicalPlanError::QueryPlanningError(

0 commit comments

Comments
 (0)