From 5bcffdea9962ef9ad85231aa19add3700edf0d78 Mon Sep 17 00:00:00 2001 From: Mikhail Kot Date: Mon, 1 Jun 2026 11:54:19 +0100 Subject: [PATCH 1/2] Use Vec instead of BTreeSet for splits Signed-off-by: Mikhail Kot --- vortex-cuda/src/layout.rs | 6 +- vortex-file/src/v2/file_stats_reader.rs | 4 +- vortex-layout/src/layouts/chunked/reader.rs | 30 ++++-- vortex-layout/src/layouts/dict/reader.rs | 4 +- vortex-layout/src/layouts/flat/reader.rs | 6 +- vortex-layout/src/layouts/row_idx/mod.rs | 4 +- vortex-layout/src/layouts/struct_/reader.rs | 6 +- vortex-layout/src/layouts/zoned/reader.rs | 4 +- vortex-layout/src/reader.rs | 30 +++++- vortex-layout/src/scan/repeated_scan.rs | 9 +- vortex-layout/src/scan/scan_builder.rs | 40 ++++++-- vortex-layout/src/scan/split_by.rs | 103 ++++++++++++++++++-- vortex-layout/src/scan/splits.rs | 5 +- 13 files changed, 202 insertions(+), 49 deletions(-) diff --git a/vortex-cuda/src/layout.rs b/vortex-cuda/src/layout.rs index 76e5a5ba5fc..5cc9106c535 100644 --- a/vortex-cuda/src/layout.rs +++ b/vortex-cuda/src/layout.rs @@ -4,7 +4,6 @@ //! A CUDA-optimized flat layout that inlines small constant array buffers into layout metadata. use std::any::Any; -use std::collections::BTreeSet; use std::ops::BitAnd; use std::ops::Range; use std::sync::Arc; @@ -49,6 +48,7 @@ use vortex::layout::LayoutReader; use vortex::layout::LayoutReaderRef; use vortex::layout::LayoutRef; use vortex::layout::LayoutStrategy; +use vortex::layout::RowSplits; use vortex::layout::SplitRange; use vortex::layout::VTable; use vortex::layout::layouts::SharedArrayFuture; @@ -286,10 +286,10 @@ impl LayoutReader for CudaFlatReader { &self, _field_mask: &[FieldMask], split_range: &SplitRange, - splits: &mut BTreeSet, + splits: &mut RowSplits, ) -> VortexResult<()> { split_range.check_bounds(self.layout.row_count)?; - splits.insert(split_range.root_row_range().end); + splits.push(split_range.root_row_range().end); Ok(()) } diff --git a/vortex-file/src/v2/file_stats_reader.rs b/vortex-file/src/v2/file_stats_reader.rs index d5a1dc3e5d2..a6ab9ec79df 100644 --- a/vortex-file/src/v2/file_stats_reader.rs +++ b/vortex-file/src/v2/file_stats_reader.rs @@ -7,7 +7,6 @@ //! [`FileStatsLayoutReader`] short-circuits [`pruning_evaluation`](LayoutReader::pruning_evaluation) //! by returning an all-false mask — avoiding all downstream I/O. -use std::collections::BTreeSet; use std::ops::Range; use std::sync::Arc; @@ -32,6 +31,7 @@ use vortex_error::VortexResult; use vortex_layout::ArrayFuture; use vortex_layout::LayoutReader; use vortex_layout::LayoutReaderRef; +use vortex_layout::RowSplits; use vortex_layout::SplitRange; use vortex_mask::Mask; use vortex_session::VortexSession; @@ -158,7 +158,7 @@ impl LayoutReader for FileStatsLayoutReader { &self, field_mask: &[FieldMask], split_range: &SplitRange, - splits: &mut BTreeSet, + splits: &mut RowSplits, ) -> VortexResult<()> { self.child.register_splits(field_mask, split_range, splits) } diff --git a/vortex-layout/src/layouts/chunked/reader.rs b/vortex-layout/src/layouts/chunked/reader.rs index 6c062fdc59b..c5de2d84daf 100644 --- a/vortex-layout/src/layouts/chunked/reader.rs +++ b/vortex-layout/src/layouts/chunked/reader.rs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -use std::collections::BTreeSet; use std::future; use std::ops::Range; use std::sync::Arc; +use std::sync::LazyLock; use futures::FutureExt; use futures::TryStreamExt; @@ -30,6 +30,7 @@ use crate::LayoutReaderRef; use crate::LazyReaderChildren; use crate::layouts::chunked::ChunkedLayout; use crate::reader::LayoutReader; +use crate::reader::RowSplits; use crate::reader::SplitRange; use crate::segments::SegmentSource; @@ -42,6 +43,8 @@ pub struct ChunkedReader { chunk_offsets: Vec, } +static UNKNOWN: LazyLock> = LazyLock::new(|| Arc::from("unknown layout")); + impl ChunkedReader { pub fn new( layout: ChunkedLayout, @@ -58,9 +61,17 @@ impl ChunkedReader { chunk_offsets[nchildren] = layout.row_count(); let dtypes = vec![layout.dtype.clone(); nchildren]; - let names = (0..nchildren) - .map(|idx| Arc::from(format!("{name}.[{idx}]"))) - .collect(); + + // format!() has non-marginal overhead for short queries like random + // access benchmarks + let names = if cfg!(debug_assertions) { + (0..nchildren) + .map(|idx| Arc::from(format!("{name}.[{idx}]"))) + .collect() + } else { + vec![Arc::clone(&*UNKNOWN); nchildren] + }; + let lazy_children = LazyReaderChildren::new( Arc::clone(&layout.children), dtypes, @@ -170,7 +181,7 @@ impl LayoutReader for ChunkedReader { &self, field_mask: &[FieldMask], split_range: &SplitRange, - splits: &mut BTreeSet, + splits: &mut RowSplits, ) -> VortexResult<()> { split_range.check_bounds(self.layout.row_count())?; @@ -178,7 +189,10 @@ impl LayoutReader for ChunkedReader { return Ok(()); } - for (chunk_idx, chunk_start, child_range, _) in self.ranges(split_range.row_range()) { + let iter = self.ranges(split_range.row_range()); + splits.reserve(iter.size_hint().0); + + for (chunk_idx, chunk_start, child_range, _) in iter { let child = self.chunk_reader(chunk_idx)?; let child_row_offset = split_range .row_offset() @@ -189,7 +203,7 @@ impl LayoutReader for ChunkedReader { child.register_splits(field_mask, &child_split_range, splits)?; // Register the split indicating the end of this chunk - splits.insert( + splits.push( split_range .row_offset() .checked_add(chunk_start + child_split_range.row_range().end) @@ -448,7 +462,7 @@ mod test { .splits(reader.as_ref(), &row_range, &[FieldMask::All]) .unwrap(); - assert_eq!(splits, expected.into_iter().collect()); + assert_eq!(splits, expected.into_iter().collect::>()); } #[rstest] diff --git a/vortex-layout/src/layouts/dict/reader.rs b/vortex-layout/src/layouts/dict/reader.rs index 96f12d53ece..5687274bd90 100644 --- a/vortex-layout/src/layouts/dict/reader.rs +++ b/vortex-layout/src/layouts/dict/reader.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -use std::collections::BTreeSet; use std::ops::BitAnd; use std::ops::Range; use std::sync::Arc; @@ -32,6 +31,7 @@ use vortex_utils::aliases::dash_map::DashMap; use super::DictLayout; use crate::LayoutReader; use crate::LayoutReaderRef; +use crate::RowSplits; use crate::SplitRange; use crate::layouts::SharedArrayFuture; use crate::segments::SegmentSource; @@ -168,7 +168,7 @@ impl LayoutReader for DictReader { &self, field_mask: &[FieldMask], split_range: &SplitRange, - splits: &mut BTreeSet, + splits: &mut RowSplits, ) -> VortexResult<()> { self.codes.register_splits(field_mask, split_range, splits) } diff --git a/vortex-layout/src/layouts/flat/reader.rs b/vortex-layout/src/layouts/flat/reader.rs index 8817fe5c678..954bbe5ab5c 100644 --- a/vortex-layout/src/layouts/flat/reader.rs +++ b/vortex-layout/src/layouts/flat/reader.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -use std::collections::BTreeSet; use std::ops::BitAnd; use std::ops::Range; use std::sync::Arc; @@ -24,6 +23,7 @@ use vortex_session::VortexSession; use crate::layouts::SharedArrayFuture; use crate::layouts::flat::FlatLayout; use crate::reader::LayoutReader; +use crate::reader::RowSplits; use crate::reader::SplitRange; use crate::segments::SegmentSource; @@ -105,10 +105,10 @@ impl LayoutReader for FlatReader { &self, _field_mask: &[FieldMask], split_range: &SplitRange, - splits: &mut BTreeSet, + splits: &mut RowSplits, ) -> VortexResult<()> { split_range.check_bounds(self.layout.row_count)?; - splits.insert(split_range.root_row_range().end); + splits.push(split_range.root_row_range().end); Ok(()) } diff --git a/vortex-layout/src/layouts/row_idx/mod.rs b/vortex-layout/src/layouts/row_idx/mod.rs index 18fc62b05cd..90570101827 100644 --- a/vortex-layout/src/layouts/row_idx/mod.rs +++ b/vortex-layout/src/layouts/row_idx/mod.rs @@ -3,7 +3,6 @@ mod expr; -use std::collections::BTreeSet; use std::fmt::Display; use std::fmt::Formatter; use std::ops::BitAnd; @@ -43,6 +42,7 @@ use vortex_utils::aliases::dash_map::DashMap; use crate::ArrayFuture; use crate::LayoutReader; +use crate::RowSplits; use crate::SplitRange; use crate::layouts::partitioned::PartitionedExprEval; @@ -172,7 +172,7 @@ impl LayoutReader for RowIdxLayoutReader { &self, field_mask: &[FieldMask], split_range: &SplitRange, - splits: &mut BTreeSet, + splits: &mut RowSplits, ) -> VortexResult<()> { self.child.register_splits(field_mask, split_range, splits) } diff --git a/vortex-layout/src/layouts/struct_/reader.rs b/vortex-layout/src/layouts/struct_/reader.rs index 156c7456ed6..6364d40b442 100644 --- a/vortex-layout/src/layouts/struct_/reader.rs +++ b/vortex-layout/src/layouts/struct_/reader.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -use std::collections::BTreeSet; use std::ops::Range; use std::sync::Arc; use std::sync::OnceLock; @@ -43,6 +42,7 @@ use crate::ArrayFuture; use crate::LayoutReader; use crate::LayoutReaderRef; use crate::LazyReaderChildren; +use crate::RowSplits; use crate::SplitRange; use crate::layouts::partitioned::PartitionedExprEval; use crate::layouts::struct_::StructLayout; @@ -240,10 +240,10 @@ impl LayoutReader for StructReader { &self, field_mask: &[FieldMask], split_range: &SplitRange, - splits: &mut BTreeSet, + splits: &mut RowSplits, ) -> VortexResult<()> { // In the case of an empty struct, we need to register the end split. - splits.insert(split_range.root_row_range().end); + splits.push(split_range.root_row_range().end); // Register splits for the validity child, if there is one if let Some(validity_ref) = self.validity()? { diff --git a/vortex-layout/src/layouts/zoned/reader.rs b/vortex-layout/src/layouts/zoned/reader.rs index a73ed6a547c..7c3e7fbd1fe 100644 --- a/vortex-layout/src/layouts/zoned/reader.rs +++ b/vortex-layout/src/layouts/zoned/reader.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -use std::collections::BTreeSet; use std::ops::BitAnd; use std::ops::Range; use std::sync::Arc; @@ -23,6 +22,7 @@ use vortex_session::VortexSession; use crate::LayoutReader; use crate::LayoutReaderRef; use crate::LazyReaderChildren; +use crate::RowSplits; use crate::SplitRange; use crate::layouts::zoned::ZonedLayout; use crate::layouts::zoned::pruning::PruningState; @@ -109,7 +109,7 @@ impl LayoutReader for ZonedReader { &self, field_mask: &[FieldMask], split_range: &SplitRange, - splits: &mut BTreeSet, + splits: &mut RowSplits, ) -> VortexResult<()> { self.data_child()? .register_splits(field_mask, split_range, splits) diff --git a/vortex-layout/src/reader.rs b/vortex-layout/src/reader.rs index b40339e3946..5706fc0d964 100644 --- a/vortex-layout/src/reader.rs +++ b/vortex-layout/src/reader.rs @@ -2,7 +2,6 @@ // SPDX-FileCopyrightText: Copyright the Vortex contributors use std::any::Any; -use std::collections::BTreeSet; use std::ops::Range; use std::sync::Arc; @@ -93,6 +92,33 @@ impl SplitRange { } } +/// A collection of row split points +pub struct RowSplits(Vec); + +impl RowSplits { + /// Add row to splits + pub fn push(&mut self, row: u64) { + self.0.push(row); + } + + /// Reserve space for "additional" elements + pub fn reserve(&mut self, additional: usize) { + self.0.reserve(additional); + } + + /// Create a new RowSplits with preallocated "capacity" + pub(crate) fn new_capacity(capacity: usize) -> Self { + Self(Vec::with_capacity(capacity)) + } + + pub(crate) fn into_sorted_deduped(mut self) -> Vec { + self.0.sort_unstable(); + self.0.dedup(); + self.0.shrink_to_fit(); + self.0 + } +} + /// A [`LayoutReader`] is used to read a [`crate::Layout`] in a way that can cache state across multiple /// evaluation operations. pub trait LayoutReader: 'static + Send + Sync { @@ -113,7 +139,7 @@ pub trait LayoutReader: 'static + Send + Sync { &self, field_mask: &[FieldMask], split_range: &SplitRange, - splits: &mut BTreeSet, + splits: &mut RowSplits, ) -> VortexResult<()>; /// Returns a mask where all false values are proven to be false in the given expression. diff --git a/vortex-layout/src/scan/repeated_scan.rs b/vortex-layout/src/scan/repeated_scan.rs index 70b62bdb943..681f33639bc 100644 --- a/vortex-layout/src/scan/repeated_scan.rs +++ b/vortex-layout/src/scan/repeated_scan.rs @@ -136,16 +136,19 @@ impl RepeatedScan { let row_range = intersect_ranges(row_range.as_ref(), selection_range); let ranges = match &self.splits { - Splits::Natural(btree_set) => { + Splits::Natural(vec) => { + debug_assert!(vec.is_sorted()); let splits_iter = match row_range { - None => Either::Left(btree_set.iter().copied()), + None => Either::Left(vec.iter().copied()), Some(range) => { if range.is_empty() { return Ok(Vec::new()); } + let lo = vec.partition_point(|&x| x < range.start); + let hi = vec.partition_point(|&x| x < range.end); Either::Right( iter::once(range.start) - .chain(btree_set.range(range.clone()).copied()) + .chain(vec[lo..hi].iter().copied()) .chain(iter::once(range.end)), ) } diff --git a/vortex-layout/src/scan/scan_builder.rs b/vortex-layout/src/scan/scan_builder.rs index 71dfaac0c73..a46bfa14e2f 100644 --- a/vortex-layout/src/scan/scan_builder.rs +++ b/vortex-layout/src/scan/scan_builder.rs @@ -450,7 +450,6 @@ fn to_field_mask(field: FieldName) -> FieldMask { #[cfg(test)] mod test { - use std::collections::BTreeSet; use std::ops::Range; use std::pin::Pin; use std::sync::Arc; @@ -483,6 +482,7 @@ mod test { use super::ScanBuilder; use crate::ArrayFuture; use crate::LayoutReader; + use crate::RowSplits; use crate::SplitRange; use crate::scan::test::SCAN_SESSION; use crate::scan::test::session_with_handle; @@ -523,10 +523,10 @@ mod test { &self, _field_mask: &[FieldMask], split_range: &SplitRange, - splits: &mut BTreeSet, + splits: &mut RowSplits, ) -> VortexResult<()> { self.register_splits_calls.fetch_add(1, Ordering::Relaxed); - splits.insert(split_range.root_row_range().end); + splits.push(split_range.root_row_range().end); Ok(()) } @@ -612,11 +612,11 @@ mod test { &self, _field_mask: &[FieldMask], split_range: &SplitRange, - splits: &mut BTreeSet, + splits: &mut RowSplits, ) -> VortexResult<()> { self.register_splits_calls.fetch_add(1, Ordering::Relaxed); for split in (split_range.row_range().start + 1)..=split_range.row_range().end { - splits.insert(split_range.row_offset() + split); + splits.push(split_range.row_offset() + split); } Ok(()) } @@ -725,11 +725,11 @@ mod test { &self, _field_mask: &[FieldMask], split_range: &SplitRange, - splits: &mut BTreeSet, + splits: &mut RowSplits, ) -> VortexResult<()> { self.register_splits_calls.fetch_add(1, Ordering::Relaxed); let _guard = self.gate.lock(); - splits.insert(split_range.root_row_range().end); + splits.push(split_range.root_row_range().end); Ok(()) } @@ -806,4 +806,30 @@ mod test { drop(runtime); } + + #[test] + fn into_stream_with_row_range() -> VortexResult<()> { + let mut ctx = LEGACY_SESSION.create_execution_ctx(); + let calls = Arc::new(AtomicUsize::new(0)); + let reader = Arc::new(SplittingLayoutReader::new(Arc::clone(&calls))); + + let runtime = SingleThreadRuntime::default(); + let session = session_with_handle(runtime.handle()); + + let stream = ScanBuilder::new(session, reader) + .with_row_range(1..3) + .into_stream()?; + let mut iter = runtime.block_on_stream(stream); + + let mut values = Vec::new(); + for chunk in &mut iter { + let prim = chunk?.execute::(&mut ctx)?; + values.extend(prim.into_buffer::().iter().copied()); + } + + assert_eq!(calls.load(Ordering::Relaxed), 1); + assert_eq!(values.as_ref(), [1, 2]); + + Ok(()) + } } diff --git a/vortex-layout/src/scan/split_by.rs b/vortex-layout/src/scan/split_by.rs index 8cc6e2e095a..34b0edbf4c8 100644 --- a/vortex-layout/src/scan/split_by.rs +++ b/vortex-layout/src/scan/split_by.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -use std::collections::BTreeSet; use std::iter::once; use std::ops::Range; @@ -9,6 +8,7 @@ use vortex_array::dtype::FieldMask; use vortex_error::VortexResult; use crate::LayoutReader; +use crate::RowSplits; use crate::SplitRange; /// Defines how the Vortex file is split into batches for reading. @@ -32,20 +32,19 @@ impl SplitBy { layout_reader: &dyn LayoutReader, row_range: &Range, field_mask: &[FieldMask], - ) -> VortexResult> { + ) -> VortexResult> { Ok(match *self { SplitBy::Layout => { - let mut row_splits = BTreeSet::::new(); - row_splits.insert(row_range.start); - - // Register all splits in the row range for all layouts that are needed - // to read the field mask. + // We usually have under 100 splits so reserving upfront saves + // us some allocations + let mut row_splits = RowSplits::new_capacity(128); + row_splits.push(row_range.start); layout_reader.register_splits( field_mask, &SplitRange::root(row_range.clone())?, &mut row_splits, )?; - row_splits + row_splits.into_sorted_deduped() } SplitBy::RowCount(n) => row_range .clone() @@ -58,18 +57,28 @@ impl SplitBy { #[cfg(test)] mod test { + use std::any::Any; use std::sync::Arc; + use futures::future::BoxFuture; use vortex_array::ArrayContext; + use vortex_array::ArrayRef; use vortex_array::IntoArray; + use vortex_array::MaskFuture; + use vortex_array::dtype::DType; use vortex_array::dtype::FieldPath; + use vortex_array::dtype::Nullability; + use vortex_array::dtype::PType; + use vortex_array::expr::Expression; use vortex_buffer::buffer; use vortex_io::runtime::single::block_on; use vortex_io::session::RuntimeSessionExt; + use vortex_mask::Mask; use super::*; use crate::LayoutReaderRef; use crate::LayoutStrategy; + use crate::RowSplits; use crate::layouts::flat::writer::FlatLayoutStrategy; use crate::scan::test::SCAN_SESSION; use crate::segments::TestSegments; @@ -113,7 +122,7 @@ mod test { &[FieldMask::Exact(FieldPath::root())], ) .unwrap(); - assert_eq!(splits, [0, 10].into_iter().collect()); + assert_eq!(splits, vec![0u64, 10]); } #[test] @@ -127,6 +136,80 @@ mod test { &[FieldMask::Exact(FieldPath::root())], ) .unwrap(); - assert_eq!(splits, [0, 3, 6, 9, 10].into_iter().collect()); + assert_eq!(splits, vec![0u64, 3, 6, 9, 10]); + } + + #[test] + fn test_layout_splits_dedup() { + struct DupReader { + name: Arc, + dtype: DType, + } + + impl LayoutReader for DupReader { + fn name(&self) -> &Arc { + &self.name + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn dtype(&self) -> &DType { + &self.dtype + } + + fn row_count(&self) -> u64 { + 10 + } + + fn register_splits( + &self, + _field_mask: &[FieldMask], + split_range: &SplitRange, + splits: &mut RowSplits, + ) -> VortexResult<()> { + splits.push(split_range.row_offset() + 5); + splits.push(split_range.row_offset() + 5); + splits.push(split_range.root_row_range().end); + Ok(()) + } + + fn pruning_evaluation( + &self, + _: &Range, + _: &Expression, + _: Mask, + ) -> VortexResult { + unimplemented!() + } + + fn filter_evaluation( + &self, + _: &Range, + _: &Expression, + _: MaskFuture, + ) -> VortexResult { + unimplemented!() + } + + fn projection_evaluation( + &self, + _: &Range, + _: &Expression, + _: MaskFuture, + ) -> VortexResult>> { + unimplemented!() + } + } + + let reader = DupReader { + name: Arc::from("dup"), + dtype: DType::Primitive(PType::U8, Nullability::NonNullable), + }; + let splits = SplitBy::Layout + .splits(&reader, &(0..10), &[FieldMask::All]) + .unwrap(); + assert_eq!(splits, vec![0u64, 5, 10]); } } diff --git a/vortex-layout/src/scan/splits.rs b/vortex-layout/src/scan/splits.rs index 16255efb66b..cf5fa7a0759 100644 --- a/vortex-layout/src/scan/splits.rs +++ b/vortex-layout/src/scan/splits.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors -use std::collections::BTreeSet; use std::ops::Range; use vortex_scan::selection::Selection; @@ -18,7 +17,9 @@ const MIN_GAP_BETWEEN_RANGES: u64 = IDEAL_SPLIT_SIZE / 2; pub enum Splits { /// Natural splits computed by the layout reader (e.g., computing splits across different-sized /// column chunks). - Natural(BTreeSet), + /// + /// The vec is sorted in ascending order and deduplicated. + Natural(Vec), /// Exact split ranges. /// From ab61217964e07ce7a5fef699165608f6232c4063 Mon Sep 17 00:00:00 2001 From: Mikhail Kot Date: Mon, 1 Jun 2026 14:54:44 +0100 Subject: [PATCH 2/2] Update vortex-layout/src/layouts/chunked/reader.rs Co-authored-by: Joe Isaacs Signed-off-by: Mikhail Kot --- vortex-layout/src/layouts/chunked/reader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vortex-layout/src/layouts/chunked/reader.rs b/vortex-layout/src/layouts/chunked/reader.rs index c5de2d84daf..39bd8b27b7c 100644 --- a/vortex-layout/src/layouts/chunked/reader.rs +++ b/vortex-layout/src/layouts/chunked/reader.rs @@ -43,7 +43,7 @@ pub struct ChunkedReader { chunk_offsets: Vec, } -static UNKNOWN: LazyLock> = LazyLock::new(|| Arc::from("unknown layout")); +static UNKNOWN: LazyLock> = LazyLock::new(|| Arc::from("chunked-child")); impl ChunkedReader { pub fn new(