Skip to content

Commit ab51863

Browse files
genezhangclaude
andcommitted
fix: rewrite denormalized node aliases to edge aliases in WITH CTE body
When a denormalized schema embeds node properties in the edge table, the FROM clause aliases the table with the edge alias (e.g., `flights AS r`). Property access on denormalized nodes (e.g., `a.city`) must be rewritten to use the edge alias (`r.OriginCityName`). The property name mapping was already handled by `rewrite_expression_with_property_mapping`, but the table alias was not being rewritten in the WITH CTE rendering path. Add `rewrite_denormalized_aliases_in_expr()` that uses `get_properties_with_table_alias()` to detect denormalized nodes and rewrite their aliases in SELECT items and GROUP BY expressions during CTE body construction. Fixes 3 denormalized WITH aggregation integration tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 10f4027 commit ab51863

3 files changed

Lines changed: 47 additions & 7 deletions

File tree

src/render_plan/plan_builder_utils.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8284,6 +8284,43 @@ pub(crate) fn build_chained_with_match_cte_plan(
82848284
// Performance optimization: Wrap non-ID columns with ANY() when aggregating
82858285
// This allows GROUP BY to only include ID column (more efficient)
82868286

8287+
// Rewrite denormalized node aliases to edge aliases in RenderExpr.
8288+
// For denormalized schemas (e.g., Airport in flights table),
8289+
// PropertyAccessExp(a, OriginCityName) must become
8290+
// PropertyAccessExp(r, OriginCityName) because FROM is `flights AS r`.
8291+
fn rewrite_denormalized_aliases_in_expr(expr: &mut RenderExpr, plan: &LogicalPlan) {
8292+
match expr {
8293+
RenderExpr::PropertyAccessExp(prop) => {
8294+
if let Ok((_, Some(edge_alias))) = plan.get_properties_with_table_alias(&prop.table_alias.0) {
8295+
if edge_alias != prop.table_alias.0 {
8296+
log::info!(
8297+
"🔧 Denormalized alias rewrite in WITH: '{}.{}' → '{}.{}'",
8298+
prop.table_alias.0, prop.column.raw(),
8299+
edge_alias, prop.column.raw()
8300+
);
8301+
prop.table_alias = crate::render_plan::render_expr::TableAlias(edge_alias);
8302+
}
8303+
}
8304+
}
8305+
RenderExpr::AggregateFnCall(agg) => {
8306+
for arg in &mut agg.args {
8307+
rewrite_denormalized_aliases_in_expr(arg, plan);
8308+
}
8309+
}
8310+
RenderExpr::ScalarFnCall(f) => {
8311+
for arg in &mut f.args {
8312+
rewrite_denormalized_aliases_in_expr(arg, plan);
8313+
}
8314+
}
8315+
RenderExpr::OperatorApplicationExp(op) => {
8316+
for operand in &mut op.operands {
8317+
rewrite_denormalized_aliases_in_expr(operand, plan);
8318+
}
8319+
}
8320+
_ => {}
8321+
}
8322+
}
8323+
82878324
// Extract UNWIND alias from plan if present — UNWIND aliases are simple
82888325
// ARRAY JOIN column references, not table aliases to expand.
82898326
// Must recurse through wrapping nodes (Filter, Projection, etc.)
@@ -8438,6 +8475,9 @@ pub(crate) fn build_chained_with_match_cte_plan(
84388475

84398476
let expr_result: Result<RenderExpr, _> = expanded_expr.try_into();
84408477
expr_result.ok().map(|mut expr| {
8478+
// Rewrite denormalized node aliases (e.g., a → r)
8479+
rewrite_denormalized_aliases_in_expr(&mut expr, plan_to_render);
8480+
84418481
// 🔧 FIX: VLP CTE column rewriting for non-TableAlias WITH items
84428482
// When FROM is a VLP/multi-type CTE, PropertyAccess references
84438483
// (e.g., message.content) must be rewritten to CTE columns
@@ -8740,7 +8780,10 @@ pub(crate) fn build_chained_with_match_cte_plan(
87408780
ExpressionRewriteContext::new(plan_to_render)
87418781
};
87428782
let rewritten = rewrite_expression_with_property_mapping(&item.expression, &rewrite_ctx);
8743-
let expr_vec: Vec<RenderExpr> = rewritten.try_into().ok().into_iter().collect();
8783+
let expr_vec: Vec<RenderExpr> = rewritten.try_into().ok().map(|mut expr: RenderExpr| {
8784+
rewrite_denormalized_aliases_in_expr(&mut expr, plan_to_render);
8785+
expr
8786+
}).into_iter().collect();
87448787
expr_vec
87458788
}
87468789
}

tests/integration/query_patterns/test_pattern_matrix.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,7 @@ def test_group_by_2(self):
817817
result = execute_query(query, "ontime_flights")
818818
assert "error" not in result, f"Query failed: {result}"
819819

820-
@pytest.mark.xfail(reason="Denormalized schema: node property resolution or VLP CTE gap")
820+
@pytest.mark.xfail(reason="Denormalized schema: 'airport' property not in schema mapping")
821821
def test_with_agg_0(self):
822822
"""
823823
WITH clause aggregation
@@ -827,7 +827,6 @@ def test_with_agg_0(self):
827827
result = execute_query(query, "ontime_flights")
828828
assert "error" not in result, f"Query failed: {result}"
829829

830-
@pytest.mark.xfail(reason="Denormalized schema: node property resolution or VLP CTE gap")
831830
def test_with_agg_1(self):
832831
"""
833832
WITH clause aggregation
@@ -837,7 +836,7 @@ def test_with_agg_1(self):
837836
result = execute_query(query, "ontime_flights")
838837
assert "error" not in result, f"Query failed: {result}"
839838

840-
@pytest.mark.xfail(reason="Denormalized schema: node property resolution or VLP CTE gap")
839+
@pytest.mark.xfail(reason="Denormalized schema: 'airport' property not in schema mapping")
841840
def test_with_agg_2(self):
842841
"""
843842
WITH clause aggregation

tests/integration/query_patterns/test_pattern_schema_matrix.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,6 @@ def test_group_by_2(self):
836836
result = execute_query(query, "ontime_flights")
837837
assert "error" not in result, f"Query failed: {result}"
838838

839-
@pytest.mark.xfail(reason="Bug: WITH aggregation generates incorrect SQL")
840839
def test_with_agg_0(self):
841840
"""
842841
WITH clause aggregation
@@ -846,7 +845,6 @@ def test_with_agg_0(self):
846845
result = execute_query(query, "ontime_flights")
847846
assert "error" not in result, f"Query failed: {result}"
848847

849-
@pytest.mark.xfail(reason="Bug: WITH aggregation generates incorrect SQL")
850848
def test_with_agg_1(self):
851849
"""
852850
WITH clause aggregation
@@ -856,7 +854,7 @@ def test_with_agg_1(self):
856854
result = execute_query(query, "ontime_flights")
857855
assert "error" not in result, f"Query failed: {result}"
858856

859-
@pytest.mark.xfail(reason="Bug: WITH aggregation generates incorrect SQL")
857+
@pytest.mark.xfail(reason="Bug: denormalized node_id property not mapped to DB column in WITH")
860858
def test_with_agg_2(self):
861859
"""
862860
WITH clause aggregation

0 commit comments

Comments
 (0)