This is an automated email from the ASF dual-hosted git repository.
github-bot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git
The following commit(s) were added to refs/heads/main by this push:
new 2b986c8872 Fix index panic in unparser with mismatched stacked
projections (#21094)
2b986c8872 is described below
commit 2b986c887224d818ee30b111d6c9047a45d20c35
Author: Matthew Kim <[email protected]>
AuthorDate: Mon Mar 23 14:03:53 2026 -0400
Fix index panic in unparser with mismatched stacked projections (#21094)
## Rationale for this change
This PR adds a length guard in `subquery_alias_inner_query_and_columns`
to prevent a panic when outer and inner projections have different
expression counts
Without this fix, optimizer passes like `CommonSubexprEliminate` can
produce stacked projections where the inner projections has more exprs
than the outer. The function iterates over `inner_projection.expr` by
index and uses the _same_ index to access `outer_projections.expr`,
causing an index oob panic
This PR bails out early when the lengths don't match. This is consistent
with all the other early returns in the function that reject
non-matching plan shapes
---
datafusion/sql/src/unparser/rewrite.rs | 54 ++++++++++++++++++++++++++++++++++
1 file changed, 54 insertions(+)
diff --git a/datafusion/sql/src/unparser/rewrite.rs
b/datafusion/sql/src/unparser/rewrite.rs
index e3b644f33f..723cdcd687 100644
--- a/datafusion/sql/src/unparser/rewrite.rs
+++ b/datafusion/sql/src/unparser/rewrite.rs
@@ -318,6 +318,10 @@ pub(super) fn subquery_alias_inner_query_and_columns(
// Check if the inner projection and outer projection have a matching
pattern like
// Projection: j1.j1_id AS id
// Projection: j1.j1_id
+ if outer_projections.expr.len() != inner_projection.expr.len() {
+ return (plan, vec![]);
+ }
+
for (i, inner_expr) in inner_projection.expr.iter().enumerate() {
let Expr::Alias(outer_alias) = &outer_projections.expr[i] else {
return (plan, vec![]);
@@ -490,3 +494,53 @@ impl TreeNodeRewriter for TableAliasRewriter<'_> {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use arrow::datatypes::{DataType, Field};
+ use datafusion_expr::{LogicalPlanBuilder, col, table_scan};
+
+ // this is a regression test: when the outer projection has fewer
expressions than
+ // the inner projection, `subquery_alias_inner_query_and_columns` must not
panic
+ // with an index oob error
+ // note: this happens when optimizer passes (e.g. CommonSubexprEliminate)
+ // insert an inner projection with extra columns that a subsequent
projection narrows
+ // back down
+ #[test]
+ fn test_stacked_projections_mismatched_lengths_no_panic() {
+ let schema = Schema::new(vec![
+ Field::new("id", DataType::Int32, false),
+ Field::new("name", DataType::Utf8, false),
+ ]);
+
+ // Inner projection has 2 expressions, outer has 0 (empty).
+ let inner_plan = LogicalPlanBuilder::from(
+ table_scan(Some("t"), &schema, Some(vec![0, 1]))
+ .unwrap()
+ .build()
+ .unwrap(),
+ )
+ .project(vec![col("t.id"), col("t.name")])
+ .unwrap()
+ .build()
+ .unwrap();
+
+ // Build an empty outer projection over the inner.
+ let outer_plan = LogicalPlanBuilder::from(inner_plan)
+ .project(Vec::<Expr>::new())
+ .unwrap()
+ .alias("sub")
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let LogicalPlan::SubqueryAlias(subquery_alias) = &outer_plan else {
+ panic!("expected SubqueryAlias");
+ };
+
+ // should return early without panicking
+ let (_plan, columns) =
subquery_alias_inner_query_and_columns(subquery_alias);
+ assert!(columns.is_empty());
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]