This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[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 b9968a06ea refactor: de-duplicate parquet read plan construction 
(#23426)
b9968a06ea is described below

commit b9968a06eadcc52d970b617d4845034a251184ac
Author: Adrian Garcia Badaracco <[email protected]>
AuthorDate: Thu Jul 9 21:13:05 2026 -0500

    refactor: de-duplicate parquet read plan construction (#23426)
    
    ## Which issue does this PR close?
    
    - N/A — follow-up cleanup requested in review of #23396, in two
    comments:
    
[`root_level_plan`](https://github.com/apache/datafusion/pull/23396#discussion_r3552883479)
    and
    
[`assemble_read_plan`](https://github.com/apache/datafusion/pull/23396#discussion_r3552881821).
    
    Two independent commits, each green on its own; best reviewed commit by
    commit.
    
    ## Rationale for this change
    
    @mbutrovich spotted two pieces of duplication in
    `projection_read_plan.rs` while reviewing #23396, both deferred out of
    that (move-only) PR:
    
    1. `build_projection_read_plan` has three early-return paths — the
    all-plain-columns fast path, the no-struct-columns fast path, and the
    no-struct-accesses fallback. Each repeated the same
    `ProjectionMask::roots` + `Schema::project` +
    construct-`ParquetReadPlan` sequence.
    
    2. `build_projection_read_plan` and
    `row_filter::build_parquet_read_plan` both end in the same chain to
    build a leaf-level plan:
    
       ```
    leaf_indices_for_roots → resolve_struct_field_leaves → extend/sort/dedup
           → ProjectionMask::leaves → build_filter_schema → ParquetReadPlan
       ```
    
    They differ only in that the row filter also sizes the resulting leaves
    (`required_bytes`) and returns an `Option`.
    
    ## What changes are included in this PR?
    
    **Commit 1 — `root_level_plan`:** a new private helper taking sorted,
    deduplicated root indices and decoding every leaf below each root. The
    three early-return paths delegate to it. Net −9 lines.
    
    **Commit 2 — `assemble_read_plan`:** shared by both callers. It returns
    the `ParquetReadPlan` plus the resolved leaf indices, which is what lets
    the row filter keep computing `required_bytes` without duplicating leaf
    resolution. `build_parquet_read_plan` shrinks from ~30 lines to ~10.
    `leaf_indices_for_roots`, `resolve_struct_field_leaves` and
    `build_filter_schema` become private to `projection_read_plan`, since
    `assemble_read_plan` is now their only caller.
    
    No behavior change in either commit.
    
    ## Are these changes tested?
    
    Yes, by existing tests: `datafusion-datasource-parquet` unit tests (158
    pass) and the `parquet_integration` suite in `datafusion` (213 pass),
    both unchanged and both green at each commit. The row-filter path
    touched by commit 2 is covered by `parquet::filter_pushdown` and the
    struct-field pushdown tests in `row_filter.rs`.
    
    ## Are there any user-facing changes?
    
    No.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    https://claude.ai/code/session_01BsRGsN18YBCji5iFTQmWpr
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../datasource-parquet/src/projection_read_plan.rs | 117 ++++++++++++---------
 datafusion/datasource-parquet/src/row_filter.rs    |  31 +-----
 2 files changed, 69 insertions(+), 79 deletions(-)

diff --git a/datafusion/datasource-parquet/src/projection_read_plan.rs 
b/datafusion/datasource-parquet/src/projection_read_plan.rs
index 46caed6616..c9d8beab14 100644
--- a/datafusion/datasource-parquet/src/projection_read_plan.rs
+++ b/datafusion/datasource-parquet/src/projection_read_plan.rs
@@ -363,18 +363,7 @@ pub(crate) fn build_projection_read_plan(
         root_indices.sort_unstable();
         root_indices.dedup();
 
-        let projection_mask =
-            ProjectionMask::roots(schema_descr, root_indices.iter().copied());
-        let projected_schema = Arc::new(
-            file_schema
-                .project(&root_indices)
-                .expect("valid column indices"),
-        );
-
-        return ParquetReadPlan {
-            projection_mask,
-            projected_schema,
-        };
+        return root_level_plan(&root_indices, file_schema, schema_descr);
     }
 
     // secondary fast path: if the schema has no struct columns, we can skip
@@ -393,19 +382,7 @@ pub(crate) fn build_projection_read_plan(
         root_indices.sort_unstable();
         root_indices.dedup();
 
-        let projection_mask =
-            ProjectionMask::roots(schema_descr, root_indices.iter().copied());
-
-        let projected_schema = Arc::new(
-            file_schema
-                .project(&root_indices)
-                .expect("valid column indices"),
-        );
-
-        return ParquetReadPlan {
-            projection_mask,
-            projected_schema,
-        };
+        return root_level_plan(&root_indices, file_schema, schema_descr);
     }
 
     let mut all_root_indices = Vec::new();
@@ -426,38 +403,74 @@ pub(crate) fn build_projection_read_plan(
     // when no struct field accesses were found, fall back to root-level 
projection
     // to match the performance of the simple path
     if all_struct_accesses.is_empty() {
-        let projection_mask =
-            ProjectionMask::roots(schema_descr, 
all_root_indices.iter().copied());
-        let projected_schema = Arc::new(
-            file_schema
-                .project(&all_root_indices)
-                .expect("valid column indices"),
-        );
-
-        return ParquetReadPlan {
-            projection_mask,
-            projected_schema,
-        };
+        return root_level_plan(&all_root_indices, file_schema, schema_descr);
     }
 
-    let leaf_indices = {
-        let mut out =
-            leaf_indices_for_roots(all_root_indices.iter().copied(), 
schema_descr);
-        let struct_leaf_indices =
-            resolve_struct_field_leaves(&all_struct_accesses, file_schema, 
schema_descr);
+    let (read_plan, _leaf_indices) = assemble_read_plan(
+        &all_root_indices,
+        &all_struct_accesses,
+        file_schema,
+        schema_descr,
+    );
 
-        out.extend_from_slice(&struct_leaf_indices);
-        out.sort_unstable();
-        out.dedup();
+    read_plan
+}
 
-        out
-    };
+/// Builds a leaf-level [`ParquetReadPlan`] covering `root_indices` in full 
plus
+/// the individual leaves reached by `struct_field_accesses`.
+///
+/// `root_indices` must be sorted, deduplicated indices into `file_schema`.
+///
+/// Also returns the resolved Parquet leaf indices, sorted and deduplicated, so
+/// callers can size the columns the decoder will read.
+pub(crate) fn assemble_read_plan(
+    root_indices: &[usize],
+    struct_field_accesses: &[StructFieldAccess],
+    file_schema: &Schema,
+    schema_descr: &SchemaDescriptor,
+) -> (ParquetReadPlan, Vec<usize>) {
+    let mut leaf_indices =
+        leaf_indices_for_roots(root_indices.iter().copied(), schema_descr);
+    leaf_indices.extend_from_slice(&resolve_struct_field_leaves(
+        struct_field_accesses,
+        file_schema,
+        schema_descr,
+    ));
+    leaf_indices.sort_unstable();
+    leaf_indices.dedup();
 
     let projection_mask =
         ProjectionMask::leaves(schema_descr, leaf_indices.iter().copied());
-
     let projected_schema =
-        build_filter_schema(file_schema, &all_root_indices, 
&all_struct_accesses);
+        build_filter_schema(file_schema, root_indices, struct_field_accesses);
+
+    (
+        ParquetReadPlan {
+            projection_mask,
+            projected_schema,
+        },
+        leaf_indices,
+    )
+}
+
+/// Builds a [`ParquetReadPlan`] that decodes whole root columns.
+///
+/// `root_indices` must be sorted, deduplicated indices into `file_schema`. 
Every
+/// leaf below each root is decoded, and the projected schema keeps each root
+/// field's full type. Callers that need to decode only some leaves of a struct
+/// root must build the plan from leaf indices instead.
+fn root_level_plan(
+    root_indices: &[usize],
+    file_schema: &Schema,
+    schema_descr: &SchemaDescriptor,
+) -> ParquetReadPlan {
+    let projection_mask =
+        ProjectionMask::roots(schema_descr, root_indices.iter().copied());
+    let projected_schema = Arc::new(
+        file_schema
+            .project(root_indices)
+            .expect("valid column indices"),
+    );
 
     ParquetReadPlan {
         projection_mask,
@@ -465,7 +478,7 @@ pub(crate) fn build_projection_read_plan(
     }
 }
 
-pub(crate) fn leaf_indices_for_roots<I>(
+fn leaf_indices_for_roots<I>(
     root_indices: I,
     schema_descr: &SchemaDescriptor,
 ) -> Vec<usize>
@@ -491,7 +504,7 @@ where
 /// For every `StructFieldAccess`, finds the leaf columns in the Parquet schema
 /// whose path matches the struct root name + field path. This avoids reading 
all
 /// leaves of a struct when only specific fields are needed
-pub(crate) fn resolve_struct_field_leaves(
+fn resolve_struct_field_leaves(
     accesses: &[StructFieldAccess],
     file_schema: &Schema,
     schema_descr: &SchemaDescriptor,
@@ -530,7 +543,7 @@ pub(crate) fn resolve_struct_field_leaves(
 /// For struct columns accessed via `get_field`, a pruned struct type is 
created
 /// containing only the fields along the access path. Note: it must match the 
schema
 /// that the Parquet reader produces when projecting specific struct leaves
-pub(crate) fn build_filter_schema(
+fn build_filter_schema(
     file_schema: &Schema,
     regular_indices: &[usize],
     struct_field_accesses: &[StructFieldAccess],
diff --git a/datafusion/datasource-parquet/src/row_filter.rs 
b/datafusion/datasource-parquet/src/row_filter.rs
index 4505f7fd62..a375e6611e 100644
--- a/datafusion/datasource-parquet/src/row_filter.rs
+++ b/datafusion/datasource-parquet/src/row_filter.rs
@@ -86,8 +86,7 @@ use datafusion_physical_plan::metrics;
 use super::ParquetFileMetrics;
 use super::supported_predicates::supports_list_predicates;
 use crate::projection_read_plan::{
-    ParquetReadPlan, PushdownChecker, PushdownColumns, build_filter_schema,
-    leaf_indices_for_roots, resolve_struct_field_leaves,
+    ParquetReadPlan, PushdownChecker, PushdownColumns, assemble_read_plan,
 };
 
 /// A "compiled" predicate passed to `ParquetRecordBatchStream` to perform
@@ -269,38 +268,16 @@ pub(crate) fn build_parquet_read_plan(
         return Ok(None);
     };
 
-    let root_indices = &required_columns.required_columns;
-
-    let mut leaf_indices =
-        leaf_indices_for_roots(root_indices.iter().copied(), schema_descr);
-
-    let struct_leaf_indices = resolve_struct_field_leaves(
+    let (read_plan, leaf_indices) = assemble_read_plan(
+        &required_columns.required_columns,
         &required_columns.struct_field_accesses,
         file_schema,
         schema_descr,
     );
-    leaf_indices.extend_from_slice(&struct_leaf_indices);
-    leaf_indices.sort_unstable();
-    leaf_indices.dedup();
 
     let required_bytes = size_of_columns(&leaf_indices, metadata)?;
 
-    let projection_mask =
-        ProjectionMask::leaves(schema_descr, leaf_indices.iter().copied());
-
-    let projected_schema = build_filter_schema(
-        file_schema,
-        root_indices,
-        &required_columns.struct_field_accesses,
-    );
-
-    Ok(Some((
-        ParquetReadPlan {
-            projection_mask,
-            projected_schema,
-        },
-        required_bytes,
-    )))
+    Ok(Some((read_plan, required_bytes)))
 }
 
 /// Checks if a predicate expression can be pushed down to the parquet decoder.


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

Reply via email to