From 7fc44c9d7e005bd9b7447712d007e18a1e716bab Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Wed, 22 Jul 2026 13:46:11 -0400 Subject: [PATCH] Read the data child directly when the zone map is empty When a zoned layout has no usable zone map (`zone_len == 0`, either a legacy zero-length zone or a layout whose aggregates this session cannot reconstruct), `new_reader` now returns the data child's reader directly instead of building a passthrough `ZonedReader`. This unifies the legacy zero-zone path with the unknown-aggregate fallback and lets us delete the now-unreachable `zone_len == 0` branch inside `ZonedReader::pruning_evaluation`. Signed-off-by: Connor Tsui Co-Authored-By: Claude Opus 4.8 --- vortex-layout/src/layouts/zoned/mod.rs | 45 ++++++++++++++++------- vortex-layout/src/layouts/zoned/reader.rs | 24 ++++++++---- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/vortex-layout/src/layouts/zoned/mod.rs b/vortex-layout/src/layouts/zoned/mod.rs index d4e5d9350be..3a4d80902f3 100644 --- a/vortex-layout/src/layouts/zoned/mod.rs +++ b/vortex-layout/src/layouts/zoned/mod.rs @@ -65,6 +65,35 @@ use crate::vtable; vtable!(Zoned); vtable!(LegacyStats); +/// Builds a reader for a zoned layout, bypassing [`ZonedReader`] when the zone map is empty +/// (`zone_len == 0`) and reading the data child directly, since there is nothing to prune with. +/// This covers both legacy zero-length zones and layouts whose aggregates the session cannot +/// reconstruct. +fn zoned_reader( + layout: &ZonedLayout, + name: Arc, + segment_source: Arc, + session: &VortexSession, + ctx: &crate::LayoutReaderContext, +) -> VortexResult { + if layout.zone_len == 0 { + return layout.children.child(0, &layout.dtype)?.new_reader( + name, + segment_source, + session, + ctx, + ); + } + + Ok(Arc::new(ZonedReader::try_new( + layout.clone(), + name, + segment_source, + session.clone(), + ctx.clone(), + )?)) +} + impl VTable for Zoned { type Layout = ZonedLayout; type Encoding = ZonedLayoutEncoding; @@ -134,13 +163,7 @@ impl VTable for Zoned { session: &VortexSession, ctx: &crate::LayoutReaderContext, ) -> VortexResult { - Ok(Arc::new(ZonedReader::try_new( - layout.clone(), - name, - segment_source, - session.clone(), - ctx.clone(), - )?)) + zoned_reader(layout, name, segment_source, session, ctx) } fn build( @@ -251,13 +274,7 @@ impl VTable for LegacyStats { session: &VortexSession, ctx: &crate::LayoutReaderContext, ) -> VortexResult { - Ok(Arc::new(ZonedReader::try_new( - layout.0.clone(), - name, - segment_source, - session.clone(), - ctx.clone(), - )?)) + zoned_reader(&layout.0, name, segment_source, session, ctx) } fn build( diff --git a/vortex-layout/src/layouts/zoned/reader.rs b/vortex-layout/src/layouts/zoned/reader.rs index 162742b6f22..a6d00c84d84 100644 --- a/vortex-layout/src/layouts/zoned/reader.rs +++ b/vortex-layout/src/layouts/zoned/reader.rs @@ -72,8 +72,8 @@ impl ZonedReader { /// Get the range of zone IDs containing a row range. pub(crate) fn zone_range(&self, row_range: &Range) -> Range { - // Caller must ensure zone_len > 0. Legacy files may deserialize with zone_len == 0, but - // pruning_evaluation disables zoned pruning for those layouts before calling this helper. + // Callers rely on `zone_len > 0`. `new_reader` never constructs a `ZonedReader` for a + // zero-length zone map (it reads the data child directly), so this holds by construction. debug_assert!(self.layout.zone_len > 0, "zone_len must be > 0"); let zone_len_u64 = self.layout.zone_len as u64; @@ -128,11 +128,6 @@ impl LayoutReader for ZonedReader { .data_child()? .pruning_evaluation(row_range, expr, mask.clone())?; - if self.layout.zone_len == 0 { - trace!("Stats pruning evaluation: skipping zoned pruning for legacy zero-length zones"); - return Ok(data_eval); - } - let Some(pruning_mask_future) = self.pruning.pruning_mask_future(expr.clone()) else { trace!("Stats pruning evaluation: not prune-able {expr}"); return Ok(data_eval); @@ -469,6 +464,21 @@ mod test { .await?; assert!(result.all_true()); + + // The bypass returns the data child's reader, so projection reads the underlying data. + let projected = reader + .projection_evaluation( + &(0..row_count), + &root(), + MaskFuture::new_true(row_count.try_into().unwrap()), + )? + .await?; + let mut ctx = array_session().create_execution_ctx(); + assert_arrays_eq!( + projected, + buffer![1i32, 2, 3, 4, 5, 6, 7, 8, 9].into_array(), + &mut ctx + ); Ok(()) }) }