@@ -138,31 +138,76 @@ impl FileScanConfig {
138138 false
139139 } ;
140140
141- if is_exact && all_non_overlapping {
142- // Truly exact: within-file ordering guaranteed and files are non-overlapping.
143- // Keep output_ordering so SortExec can be eliminated for each partition.
144- //
145- // We intentionally do NOT redistribute files across groups here.
146- // The planning-phase bin-packing may interleave file ranges across groups:
147- //
148- // Group 0: [f1(1-10), f3(21-30)] ← interleaved with group 1
149- // Group 1: [f2(11-20), f4(31-40)]
150- //
151- // This interleaving is actually beneficial because SPM pulls from both
152- // partitions concurrently, keeping parallel I/O active:
153- //
154- // SPM: pull P0 [1-10] → pull P1 [11-20] → pull P0 [21-30] → pull P1 [31-40]
155- // ^^^^^^^^^^^^ ^^^^^^^^^^^^
156- // both partitions scanning files simultaneously
157- //
158- // If we were to redistribute files consecutively:
159- // Group 0: [f1(1-10), f2(11-20)] ← all values < group 1
160- // Group 1: [f3(21-30), f4(31-40)]
141+ // Decide whether to keep `output_ordering` (i.e. let the outer
142+ // pushdown report `Exact` and drop `SortExec`).
143+ //
144+ // Two paths can produce a keep:
145+ //
146+ // 1. `is_exact && all_non_overlapping`: the source already had
147+ // validated ordering and the post-sort files still don't
148+ // overlap — Exact carries through unchanged.
149+ //
150+ // 2. `!is_exact && all_non_overlapping`: source returned
151+ // `Inexact` because pre-sort `validated_output_ordering()`
152+ // stripped the declaration (files were listed out of order
153+ // on disk). After our stats-based sort the files are now
154+ // non-overlapping — re-validate against the new file
155+ // groups and, if it passes, upgrade back to Exact so the
156+ // outer wrapper drops the `SortExec`. Without this, the
157+ // `Inexact` branch stayed Inexact even when reorder
158+ // restored a perfectly valid ordering, leaving an
159+ // unnecessary `SortExec` above the source (regression
160+ // after #21956's `column_in_file_schema` signal pushed
161+ // this scenario into the Inexact branch instead of the
162+ // `try_sort_file_groups_by_statistics` fallback).
163+ //
164+ // We intentionally do NOT redistribute files across groups here.
165+ // The planning-phase bin-packing may interleave file ranges across groups:
166+ //
167+ // Group 0: [f1(1-10), f3(21-30)] ← interleaved with group 1
168+ // Group 1: [f2(11-20), f4(31-40)]
169+ //
170+ // This interleaving is actually beneficial because SPM pulls from both
171+ // partitions concurrently, keeping parallel I/O active.
172+ let keep_ordering = match ( all_non_overlapping, is_exact) {
173+ // Files still overlap after the stats sort — the combined
174+ // stream isn't ordered, so `output_ordering` must be dropped.
175+ ( false , _) => false ,
176+ // Source already had validated ordering and the post-sort
177+ // files still don't overlap — Exact carries through.
178+ ( true , true ) => true ,
179+ // Source returned `Inexact`; re-validate against the
180+ // reordered file groups to decide whether to upgrade.
161181 //
162- // SPM would read ALL of group 0 first (values always smaller), then group 1.
163- // This degrades to single-threaded sequential I/O — the other partition
164- // sits idle the entire time, losing the parallelism benefit.
165- } else {
182+ // Same NULL guard as `try_sort_file_groups_by_statistics`:
183+ // we cannot claim Exact if any non-last file contains
184+ // NULLs in the sort columns. With NULLS LAST those
185+ // NULLs sit after all non-null rows in the file, so
186+ // when the next file's non-nulls are smaller than the
187+ // previous file's max, they'd appear *after* the NULLs
188+ // in the concatenated stream — breaking the ordering.
189+ ( true , false ) => {
190+ let projected_schema = new_config. projected_schema ( ) ?;
191+ let projection_indices = new_config
192+ . file_source
193+ . projection ( )
194+ . as_ref ( )
195+ . and_then ( |p| ordered_column_indices_from_projection ( p) ) ;
196+ if any_file_has_nulls_in_sort_columns (
197+ & new_config. file_groups ,
198+ order,
199+ & projected_schema,
200+ projection_indices. as_deref ( ) ,
201+ ) {
202+ false
203+ } else {
204+ let new_eq_props = new_config. eq_properties ( ) ;
205+ new_eq_props. ordering_satisfy ( order. iter ( ) . cloned ( ) ) ?
206+ }
207+ }
208+ } ;
209+
210+ if !keep_ordering {
166211 new_config. output_ordering = vec ! [ ] ;
167212 }
168213
0 commit comments