kosiew commented on code in PR #24686:
URL: https://github.com/apache/datafusion/pull/24686#discussion_r3879915314


##########
datafusion/optimizer/src/optimize_projections/mod.rs:
##########
@@ -522,30 +522,6 @@ fn optimize_subqueries(
 /// - `Ok(None)`: Signals that merge is not beneficial (and has not taken 
place).
 /// - `Err(error)`: An error occurred during the function call.
 fn merge_consecutive_projections(proj: Projection) -> 
Result<Transformed<Projection>> {

Review Comment:
   There is also a planning regression from removing this loop. On a 12-deep 
projection chain with `max_passes = 1`, `main` collapses the optimized logical 
plan to 1 `Projection:` node, while this PR leaves 7.
   
   This also matters with the default `max_passes = 3`, where sufficiently deep 
chains can still leave consecutive projections in the final plan. The 
integration snapshot changed by this PR is one example of that extra layer 
becoming part of the expected plan.
   
   If the equality fast path is guarded instead, as suggested in the other 
comment, the loop can stay and this regression goes away as well.



##########
datafusion/optimizer/src/optimize_projections/mod.rs:
##########
@@ -522,30 +522,6 @@ fn optimize_subqueries(
 /// - `Ok(None)`: Signals that merge is not beneficial (and has not taken 
place).
 /// - `Err(error)`: An error occurred during the function call.
 fn merge_consecutive_projections(proj: Projection) -> 
Result<Transformed<Projection>> {

Review Comment:
   I don't think the iterative merge is the root cause of the wrong results 
here. Removing it fixes the depth-3 case, but the same query still returns 
incorrect results from depth 4 onward with the default `max_passes = 3`.
   
   The issue appears to be the fast path below this code:
   
   ```rust
   if prev_projection.expr == expr {
       ...
   }
   ```
   
   That check drops the current projection whenever the expressions are 
structurally equal. This is only safe when the projection is idempotent. For 
example, `i + 1 AS i` over a child that also exposes `i` compares equal 
structurally, but evaluating it again changes the value, so dropping the 
projection removes one `+ 1` from the query.
   
   I confirmed this by keeping the iterative merge and guarding the fast path 
so it only applies to trivial projections:
   
   ```rust
   let all_trivial = expr.iter().all(|e| match e {
       Expr::Column(_) => true,
       Expr::Alias(a) => matches!(a.expr.as_ref(), Expr::Column(_)),
       _ => false,
   });
   if all_trivial && prev_projection.expr == expr {
   ```
   
   With that change, while retaining the loop, depths 1 through 12 return the 
expected results with `max_passes` set to 1, 3, or 10. I also get 763/763 
passing tests from `cargo test -p datafusion-optimizer --lib`, 504/504 files 
from the full sqllogictest suite, and this PR's regression test passes.
   
   Could we fix the fast-path condition instead of removing the iterative merge?



##########
datafusion/sqllogictest/test_files/projection.slt:
##########
@@ -217,6 +217,39 @@ SELECT column1 as a from (values (1), (2)) f where 
f.column1 = 2;
 ----
 2
 
+# Regression: one optimizer pass must preserve anonymous nested projections.
+statement ok
+CREATE TABLE nested_projection(i INT);
+
+statement ok
+INSERT INTO nested_projection VALUES (3), (4), (5);
+
+statement ok
+SET datafusion.optimizer.max_passes = 1;
+
+query I rowsort

Review Comment:
   Could we extend this regression beyond depth 3? Depth 3 is the particular 
case this change fixes, but depth 4 still returns a wrong result with the 
default optimizer pass count.
   
   I suggest testing at least depth 5 or 6 and checking the results with both 
`max_passes = 1` and the default setting. That would make the test protect the 
general nested-projection behavior rather than one depth that happens to pass 
with this implementation.
   
   It may also be useful to check the optimized plan with `EXPLAIN`, so the 
test covers both correct values and the projection-collapse behavior.



##########
datafusion/optimizer/tests/optimizer_integration.rs:
##########
@@ -792,9 +792,10 @@ fn extension_node_does_not_block_projection_pruning() -> 
Result<()> {
     OpaqueRequirementsExtension
       Sort: t.a ASC NULLS FIRST, t.ts ASC NULLS FIRST
         Projection: t.a, CAST(t.ts AS Timestamp(ms, "UTC")) AS ts
-          Filter: __common_expr_3 > TimestampMillisecond(1000, Some("UTC")) 
AND __common_expr_3 < TimestampMillisecond(2000, Some("UTC"))
-            Projection: CAST(t.ts AS Timestamp(ms, "UTC")) AS __common_expr_3, 
t.a, t.ts
-              TableScan: t projection=[a, ts], partial_filters=[CAST(t.ts AS 
Timestamp(ms, "UTC")) > TimestampMillisecond(1000, Some("UTC")), CAST(t.ts AS 
Timestamp(ms, "UTC")) < TimestampMillisecond(2000, Some("UTC"))]
+          Projection: t.a, t.ts

Review Comment:
   I don't think we should accept this extra pass-through `Projection: t.a, 
t.ts` as the new expected plan. It looks like a consequence of removing the 
iterative merge rather than an independently necessary plan change.
   
   When I keep the loop and guard the equality fast path, the previous tighter 
plan remains valid. If we take that approach, I think this snapshot change 
should be reverted.



##########
datafusion/sqllogictest/test_files/projection.slt:
##########
@@ -217,6 +217,39 @@ SELECT column1 as a from (values (1), (2)) f where 
f.column1 = 2;
 ----
 2
 
+# Regression: one optimizer pass must preserve anonymous nested projections.
+statement ok
+CREATE TABLE nested_projection(i INT);
+
+statement ok
+INSERT INTO nested_projection VALUES (3), (4), (5);
+
+statement ok
+SET datafusion.optimizer.max_passes = 1;
+
+query I rowsort

Review Comment:
   One additional test suggestion: I think a focused unit test in 
`optimize_projections` would be more valuable than adding this pattern to the 
long chained-projection benchmark. A benchmark would measure planning time, but 
it would not directly protect against this correctness bug.
   
   A unit test could specifically cover a non-idempotent projection that 
compares structurally equal to its input. It would also be useful to include a 
metadata-bearing alias variant, since alias metadata interacts with the 
trimming logic in `merge_consecutive_projections`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to