Skip to main content

vortex_layout/scan/
multi.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! A [`DataSource`] that combines multiple [`LayoutReaderRef`]s into a single scannable source.
5//!
6//! Readers may be pre-opened or deferred via [`LayoutReaderFactory`]. Deferred readers are opened
7//! concurrently during scanning using `buffer_unordered`: up to `concurrency` file opens run in
8//! parallel as spawned tasks on the session runtime. Once opened, each reader yields a single
9//! partition covering its full row range; internal I/O pipelining and chunking are handled by
10//! [`ScanBuilder`].
11//!
12//! # Schema Resolution
13//!
14//! Currently, all children must share the exact same [`DType`]. A dtype
15//! mismatch produces an error.
16//!
17//! # Future Work
18//!
19//! - **Schema union**: Allow missing columns (filled with nulls) and compatible type upcasts
20//!   across sources instead of requiring exact dtype matches.
21//! - **Hive-style partitioning**: Extract partition values from file paths (e.g. `year=2024/month=01/`)
22//!   and expose them as virtual columns.
23//! - **Virtual columns**: `filename`, `file_row_number`, `file_index`.
24//! - **Per-file statistics**: Merge column statistics across sources for planner hints.
25//! - **Error resilience**: Skip failed sources instead of aborting the entire scan.
26
27use std::any::Any;
28use std::collections::VecDeque;
29use std::ops::Range;
30use std::sync::Arc;
31
32use async_trait::async_trait;
33use futures::FutureExt;
34use futures::StreamExt;
35use futures::stream;
36use itertools::Itertools;
37use tracing::Instrument;
38use vortex_array::dtype::DType;
39use vortex_array::dtype::FieldPath;
40use vortex_array::expr::BoundExpression;
41use vortex_array::expr::stats::Precision;
42use vortex_array::stats::StatsSet;
43use vortex_array::stream::ArrayStreamAdapter;
44use vortex_array::stream::ArrayStreamExt;
45use vortex_array::stream::SendableArrayStream;
46use vortex_error::SharedVortexResult;
47use vortex_error::VortexResult;
48use vortex_error::vortex_bail;
49use vortex_error::vortex_err;
50use vortex_io::session::RuntimeSessionExt;
51use vortex_mask::Mask;
52use vortex_scan::DataSource;
53use vortex_scan::DataSourceScan;
54use vortex_scan::DataSourceScanRef;
55use vortex_scan::Partition;
56use vortex_scan::PartitionRef;
57use vortex_scan::PartitionStream;
58use vortex_scan::ScanRequest;
59use vortex_scan::selection::Selection;
60use vortex_session::VortexSession;
61use vortex_utils::parallelism::get_available_parallelism;
62
63use crate::LayoutReaderRef;
64use crate::scan::limit::RowLimit;
65use crate::scan::scan_builder::ScanBuilder;
66
67/// Default concurrency for opening deferred readers.
68const DEFAULT_CONCURRENCY: usize = 8;
69
70/// An async factory that produces a [`LayoutReaderRef`].
71///
72/// Implementations handle file opening, footer caching, and statistics-based pruning.
73/// Returns `None` if the source should be skipped (e.g., pruned based on file-level
74/// statistics before the reader is fully constructed).
75#[async_trait]
76pub trait LayoutReaderFactory: 'static + Send + Sync {
77    /// Opens the layout reader, or returns `None` if it should be skipped.
78    async fn open(&self) -> VortexResult<Option<LayoutReaderRef>>;
79}
80
81/// A [`DataSource`] that combines multiple [`LayoutReaderRef`]s into a single scannable source.
82///
83/// Readers may be pre-opened or deferred via [`LayoutReaderFactory`]. Deferred readers are opened
84/// concurrently during scanning using `buffer_unordered`, mirroring the DuckDB scan pattern: up
85/// to `concurrency` file opens run in parallel as spawned tasks on the session runtime. Once
86/// opened, each reader yields a single partition covering its full row range; internal I/O
87/// pipelining and chunking are handled by [`ScanBuilder`].
88#[derive(Clone)]
89pub struct MultiLayoutDataSource {
90    dtype: DType,
91    session: VortexSession,
92    children: Arc<[MultiLayoutChild]>,
93    concurrency: usize,
94}
95
96pub enum MultiLayoutChild {
97    Opened {
98        reader: LayoutReaderRef,
99        /// On-storage file size in bytes, if known from the listing metadata.
100        byte_size: Option<u64>,
101    },
102    Deferred {
103        factory: Arc<dyn LayoutReaderFactory>,
104        /// On-storage file size in bytes, if known from the listing metadata.
105        byte_size: Option<u64>,
106    },
107}
108
109impl MultiLayoutChild {
110    /// On-storage file size in bytes for this child, if known.
111    pub fn byte_size(&self) -> Option<u64> {
112        match self {
113            MultiLayoutChild::Opened { byte_size, .. } => *byte_size,
114            MultiLayoutChild::Deferred { byte_size, .. } => *byte_size,
115        }
116    }
117}
118
119impl MultiLayoutDataSource {
120    /// Creates a multi-layout data source with the first reader pre-opened.
121    ///
122    /// The first reader determines the dtype. Remaining readers are opened lazily during
123    /// scanning via their factories. `byte_sizes` carries the on-storage file size in bytes for
124    /// each child (first followed by remaining); pass `None` for entries where the size is
125    /// unknown. Must be empty or have length `1 + remaining.len()`.
126    pub fn new_with_first(
127        first: LayoutReaderRef,
128        remaining: Vec<Arc<dyn LayoutReaderFactory>>,
129        byte_sizes: Vec<Option<u64>>,
130        session: &VortexSession,
131    ) -> Self {
132        let dtype = first.dtype().clone();
133        let concurrency = get_available_parallelism().unwrap_or(DEFAULT_CONCURRENCY);
134
135        let total = 1 + remaining.len();
136        let mut sizes = byte_sizes;
137        if sizes.is_empty() {
138            sizes = vec![None; total];
139        }
140        debug_assert_eq!(
141            sizes.len(),
142            total,
143            "byte_sizes length must match the number of children"
144        );
145
146        let mut children = Vec::with_capacity(total);
147        let mut sizes_iter = sizes.into_iter();
148        let first_size = sizes_iter.next().unwrap_or(None);
149        children.push(MultiLayoutChild::Opened {
150            reader: first,
151            byte_size: first_size,
152        });
153        children.extend(
154            remaining
155                .into_iter()
156                .zip_eq(sizes_iter)
157                .map(|(factory, byte_size)| MultiLayoutChild::Deferred { factory, byte_size }),
158        );
159
160        Self {
161            dtype,
162            session: session.clone(),
163            children: children.into(),
164            concurrency,
165        }
166    }
167
168    /// Creates a multi-layout data source where all children are deferred.
169    ///
170    /// The dtype must be provided externally since there is no pre-opened reader to infer it
171    /// from. This avoids eagerly opening any file when the schema is already known (e.g. from
172    /// a catalog or a prior scan). `byte_sizes` carries the on-storage file size in bytes for
173    /// each factory; pass `None` for entries where the size is unknown. Must be empty or have
174    /// the same length as `factories`.
175    pub fn new_deferred(
176        dtype: DType,
177        factories: Vec<Arc<dyn LayoutReaderFactory>>,
178        byte_sizes: Vec<Option<u64>>,
179        session: &VortexSession,
180    ) -> Self {
181        let concurrency = get_available_parallelism().unwrap_or(DEFAULT_CONCURRENCY);
182
183        let mut sizes = byte_sizes;
184        if sizes.is_empty() {
185            sizes = vec![None; factories.len()];
186        }
187        debug_assert_eq!(
188            sizes.len(),
189            factories.len(),
190            "byte_sizes length must match the number of factories"
191        );
192
193        Self {
194            dtype,
195            session: session.clone(),
196            children: factories
197                .into_iter()
198                .zip_eq(sizes)
199                .map(|(factory, byte_size)| MultiLayoutChild::Deferred { factory, byte_size })
200                .collect(),
201            concurrency,
202        }
203    }
204
205    pub fn children(&self) -> &[MultiLayoutChild] {
206        &self.children
207    }
208
209    /// Sets the concurrency for opening deferred readers.
210    ///
211    /// Controls how many file opens run in parallel via `buffer_unordered`.
212    /// Defaults to the number of available CPU cores.
213    pub fn with_concurrency(mut self, concurrency: usize) -> Self {
214        self.concurrency = concurrency;
215        self
216    }
217}
218
219#[async_trait]
220impl DataSource for MultiLayoutDataSource {
221    fn dtype(&self) -> &DType {
222        &self.dtype
223    }
224
225    fn row_count(&self) -> Precision<u64> {
226        let mut sum: u64 = 0;
227        let mut opened_count: u64 = 0;
228        let mut deferred_count: u64 = 0;
229
230        for child in self.children.iter() {
231            match child {
232                MultiLayoutChild::Opened { reader, .. } => {
233                    opened_count += 1;
234                    sum = sum.saturating_add(reader.row_count());
235                }
236                MultiLayoutChild::Deferred { .. } => {
237                    deferred_count += 1;
238                }
239            }
240        }
241
242        let total_count = opened_count + deferred_count;
243        if total_count == 0 {
244            return Precision::exact(0u64);
245        }
246
247        if deferred_count == 0 {
248            Precision::exact(sum)
249        } else if let Some(avg) = sum.checked_div(opened_count) {
250            let extrapolated = avg.saturating_mul(total_count);
251            Precision::inexact(extrapolated)
252        } else {
253            Precision::Absent
254        }
255    }
256
257    fn byte_size(&self) -> Precision<u64> {
258        let total_count = self.children.len() as u64;
259        if total_count == 0 {
260            return Precision::exact(0u64);
261        }
262
263        let mut sum: u64 = 0;
264        let mut known_count: u64 = 0;
265        for child in self.children.iter() {
266            if let Some(size) = child.byte_size() {
267                sum = sum.saturating_add(size);
268                known_count += 1;
269            }
270        }
271
272        if known_count == 0 {
273            return Precision::Absent;
274        }
275
276        if known_count == total_count {
277            Precision::exact(sum)
278        } else {
279            let avg = sum / known_count;
280            let extrapolated = avg.saturating_mul(total_count);
281            Precision::inexact(extrapolated)
282        }
283    }
284
285    fn deserialize_partition(
286        &self,
287        _data: &[u8],
288        _session: &VortexSession,
289    ) -> VortexResult<PartitionRef> {
290        vortex_bail!("MultiLayoutDataSource partitions are not yet serializable")
291    }
292
293    async fn scan(&self, scan_request: ScanRequest) -> VortexResult<DataSourceScanRef> {
294        let mut ready = VecDeque::new();
295        let mut deferred = VecDeque::new();
296
297        for child in self.children.iter() {
298            match child {
299                MultiLayoutChild::Opened { reader, .. } => ready.push_back(Arc::clone(reader)),
300                MultiLayoutChild::Deferred { factory, .. } => {
301                    deferred.push_back(Arc::clone(factory))
302                }
303            }
304        }
305
306        let request = BoundScanRequest::try_new(scan_request, &self.dtype)?;
307        let dtype = request.projection.dtype().clone();
308
309        // Only unordered scans share a limit across external partitions: reservation order is
310        // completion order, which an ordered scan cannot accept. Ordered partitions each apply the
311        // limit locally and the engine trims the concatenated result.
312        let row_limit = (!request.ordered)
313            .then(|| request.limit.map(RowLimit::new))
314            .flatten();
315
316        Ok(Box::new(MultiLayoutScan {
317            session: self.session.clone(),
318            source_dtype: self.dtype.clone(),
319            dtype,
320            request,
321            row_limit,
322            ready,
323            deferred,
324            handle: self.session.handle(),
325            concurrency: self.concurrency,
326        }))
327    }
328
329    async fn field_statistics(&self, _field_path: &FieldPath) -> VortexResult<StatsSet> {
330        Ok(StatsSet::default())
331    }
332}
333
334#[derive(Clone)]
335struct BoundScanRequest {
336    projection: BoundExpression,
337    filter: SharedVortexResult<Option<BoundExpression>>,
338    row_range: Option<Range<u64>>,
339    selection: Selection,
340    partition_selection: Selection,
341    partition_range: Option<Range<u64>>,
342    ordered: bool,
343    limit: Option<u64>,
344}
345
346impl BoundScanRequest {
347    fn try_new(request: ScanRequest, dtype: &DType) -> VortexResult<Self> {
348        let ScanRequest {
349            projection,
350            filter,
351            row_range,
352            selection,
353            partition_selection,
354            partition_range,
355            ordered,
356            limit,
357        } = request;
358
359        Ok(Self {
360            projection: projection.bind(dtype)?.optimize_recursive()?,
361            filter: filter
362                .map(|expr| expr.bind(dtype)?.optimize_recursive())
363                .transpose()
364                .map_err(Arc::new),
365            row_range,
366            selection,
367            partition_selection,
368            partition_range,
369            ordered,
370            limit,
371        })
372    }
373}
374
375struct MultiLayoutScan {
376    session: VortexSession,
377    source_dtype: DType,
378    dtype: DType,
379    request: BoundScanRequest,
380    row_limit: Option<RowLimit>,
381    ready: VecDeque<LayoutReaderRef>,
382    deferred: VecDeque<Arc<dyn LayoutReaderFactory>>,
383    handle: vortex_io::runtime::Handle,
384    concurrency: usize,
385}
386
387impl DataSourceScan for MultiLayoutScan {
388    fn dtype(&self) -> &DType {
389        &self.dtype
390    }
391
392    fn partition_count(&self) -> Precision<usize> {
393        let count = self.ready.len() + self.deferred.len();
394        if self.deferred.is_empty() {
395            Precision::exact(count)
396        } else {
397            Precision::inexact(count)
398        }
399    }
400
401    fn partitions(self: Box<Self>) -> PartitionStream {
402        let Self {
403            session,
404            source_dtype,
405            dtype: _,
406            request,
407            row_limit,
408            ready,
409            deferred,
410            handle,
411            concurrency,
412        } = *self;
413
414        let ordered = request.ordered;
415
416        // Pre-opened readers are immediately available.
417        let ready_stream = stream::iter(ready).map(Ok);
418
419        // Deferred readers are opened concurrently via spawned tasks.
420        // When ordered, we use `buffered` to preserve the original partition order.
421        // When unordered, we use `buffer_unordered` to yield partitions as they open.
422        let spawned = stream::iter(deferred).map(move |factory| {
423            handle.spawn(async move {
424                factory
425                    .open()
426                    .instrument(tracing::info_span!("LayoutReaderFactory::open"))
427                    .await
428            })
429        });
430
431        let deferred_stream = if ordered {
432            spawned
433                .buffered(concurrency)
434                .filter_map(|result| async move {
435                    match result {
436                        Ok(Some(reader)) => Some(Ok(reader)),
437                        Ok(None) => None,
438                        Err(e) => Some(Err(e)),
439                    }
440                })
441                .boxed()
442        } else {
443            spawned
444                .buffer_unordered(concurrency)
445                .filter_map(|result| async move {
446                    match result {
447                        Ok(Some(reader)) => Some(Ok(reader)),
448                        Ok(None) => None,
449                        Err(e) => Some(Err(e)),
450                    }
451                })
452                .boxed()
453        };
454
455        // For each reader (ready or just-opened), generate a partition.
456        // Partition generation is synchronous (just creates structs with row ranges), so
457        // `flat_map` is appropriate here. The real I/O work happens when `execute()` is called.
458        ready_stream
459            .chain(deferred_stream)
460            .enumerate()
461            .flat_map(move |(i, reader_result)| match reader_result {
462                Ok(reader) => reader_partition(
463                    i,
464                    reader,
465                    session.clone(),
466                    &source_dtype,
467                    request.clone(),
468                    row_limit.clone(),
469                ),
470                Err(e) => stream::once(async move { Err(e) }).boxed(),
471            })
472            .boxed()
473    }
474}
475
476/// Generates a partition stream for a single layout reader.
477///
478/// Checks file-level pruning first (via `pruning_evaluation`). If the filter proves no rows
479/// can match, returns an empty stream. Otherwise, yields a single partition covering the
480/// reader's full row range.
481fn reader_partition(
482    partition_idx: usize,
483    reader: LayoutReaderRef,
484    session: VortexSession,
485    source_dtype: &DType,
486    request: BoundScanRequest,
487    row_limit: Option<RowLimit>,
488) -> PartitionStream {
489    if reader.dtype() != source_dtype {
490        let error = vortex_err!(
491            "Multi-layout reader dtype mismatch: expected {}, got {}",
492            source_dtype,
493            reader.dtype()
494        );
495        return stream::once(async move { Err(error) }).boxed();
496    }
497
498    let row_count = reader.row_count();
499    let row_range = request.row_range.clone().unwrap_or(0..row_count);
500
501    let partition_idx_u64: u64 = partition_idx as u64;
502    if let Some(range) = &request.partition_range
503        && !range.contains(&partition_idx_u64)
504    {
505        return stream::empty().boxed();
506    };
507    match &request.partition_selection {
508        Selection::IncludeByIndex(buffer)
509            if buffer.as_slice().binary_search(&partition_idx_u64).is_err() =>
510        {
511            return stream::empty().boxed();
512        }
513        Selection::ExcludeByIndex(buffer)
514            if buffer.as_slice().binary_search(&partition_idx_u64).is_ok() =>
515        {
516            return stream::empty().boxed();
517        }
518        _ => {}
519    };
520
521    // Check file-level pruning: if the filter can be proven false for the entire row range
522    // using file-level statistics, skip this reader entirely.
523    if let Ok(Some(filter)) = &request.filter {
524        let mask_len = usize::try_from(row_range.end - row_range.start).unwrap_or(usize::MAX);
525        let mask = Mask::new_true(mask_len);
526        if let Ok(pruning_future) = reader.pruning_evaluation(&row_range, filter, mask)
527            && let Some(Ok(result_mask)) = pruning_future.now_or_never()
528            && result_mask.all_false()
529        {
530            return stream::empty().boxed();
531        }
532    }
533
534    stream::once(async move {
535        Ok(Box::new(MultiLayoutPartition {
536            reader,
537            session,
538            request: BoundScanRequest {
539                row_range: Some(row_range),
540                ..request
541            },
542            row_limit,
543            index: partition_idx,
544        }) as PartitionRef)
545    })
546    .boxed()
547}
548
549/// A partition backed by a single [`LayoutReaderRef`] and a row range.
550///
551/// On `execute()`, creates a [`ScanBuilder`] over the row range, enabling
552/// internal I/O pipelining and split-level parallelism within the reader.
553struct MultiLayoutPartition {
554    reader: LayoutReaderRef,
555    session: VortexSession,
556    request: BoundScanRequest,
557    row_limit: Option<RowLimit>,
558    index: usize,
559}
560
561impl Partition for MultiLayoutPartition {
562    fn as_any(&self) -> &dyn Any {
563        self
564    }
565
566    fn index(&self) -> usize {
567        self.index
568    }
569
570    fn row_count(&self) -> Precision<u64> {
571        let Some(row_range) = self.request.row_range.as_ref() else {
572            return Precision::Absent;
573        };
574        let row_count = self.request.selection.row_count_in_range(row_range);
575        let row_count = self
576            .request
577            .limit
578            .map_or(row_count, |limit| row_count.min(limit));
579
580        let has_filter = match &self.request.filter {
581            Ok(filter) => filter.is_some(),
582            Err(_) => true,
583        };
584        if has_filter || self.row_limit.is_some() {
585            Precision::inexact(row_count)
586        } else {
587            Precision::exact(row_count)
588        }
589    }
590
591    fn byte_size(&self) -> Precision<u64> {
592        Precision::Absent
593    }
594
595    fn execute(self: Box<Self>) -> VortexResult<SendableArrayStream> {
596        let request = self.request;
597        let filter = request.filter?;
598        let mut builder = ScanBuilder::new(self.session, self.reader)
599            .with_selection(request.selection)
600            .with_projection(request.projection)
601            .with_some_filter(filter)
602            .with_ordered(request.ordered);
603        builder = match self.row_limit {
604            Some(row_limit) => builder.with_shared_limit(row_limit),
605            None => builder.with_some_limit(request.limit),
606        };
607
608        if let Some(row_range) = request.row_range {
609            builder = builder.with_row_range(row_range);
610        }
611
612        let dtype = builder.dtype()?;
613        let stream = builder.into_stream()?.boxed();
614
615        Ok(ArrayStreamExt::boxed(ArrayStreamAdapter::new(
616            dtype, stream,
617        )))
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use std::sync::Arc;
624
625    use async_trait::async_trait;
626    use futures::TryStreamExt;
627    use rstest::rstest;
628    use vortex_array::dtype::Nullability;
629    use vortex_array::dtype::PType;
630    use vortex_array::expr::eq;
631    use vortex_array::expr::lit;
632    use vortex_array::expr::root;
633    use vortex_error::VortexResult;
634    use vortex_io::runtime::BlockingRuntime;
635    use vortex_io::runtime::single::SingleThreadRuntime;
636    use vortex_scan::DataSource;
637    use vortex_scan::ScanRequest;
638
639    use super::*;
640    use crate::scan::test::TestLayoutReader;
641    use crate::scan::test::collect_scan_values;
642    use crate::scan::test::new_session;
643    use crate::scan::test::session_with_handle;
644
645    struct NeverOpened;
646
647    #[async_trait]
648    impl LayoutReaderFactory for NeverOpened {
649        async fn open(&self) -> VortexResult<Option<LayoutReaderRef>> {
650            unreachable!("byte_size must not open readers")
651        }
652    }
653
654    fn deferred_source(byte_sizes: Vec<Option<u64>>) -> MultiLayoutDataSource {
655        let factories: Vec<Arc<dyn LayoutReaderFactory>> = byte_sizes
656            .iter()
657            .map(|_| Arc::new(NeverOpened) as _)
658            .collect();
659        MultiLayoutDataSource::new_deferred(
660            DType::Bool(Nullability::NonNullable),
661            factories,
662            byte_sizes,
663            &new_session(),
664        )
665    }
666
667    #[rstest]
668    #[case::all_known(vec![Some(10), Some(20), Some(30)], Precision::exact(60u64))]
669    #[case::some_known_extrapolates(vec![Some(10), None, Some(30)], Precision::inexact(60u64))]
670    #[case::none_known(vec![None, None], Precision::Absent)]
671    #[case::no_children(vec![], Precision::exact(0u64))]
672    fn byte_size_precision(#[case] sizes: Vec<Option<u64>>, #[case] expected: Precision<u64>) {
673        assert_eq!(deferred_source(sizes).byte_size(), expected);
674    }
675
676    #[test]
677    fn filter_binding_errors_are_deferred() -> VortexResult<()> {
678        let dtype = DType::Primitive(PType::U8, Nullability::NonNullable);
679        let request = ScanRequest {
680            filter: Some(eq(root(), lit(67_i32))),
681            ..ScanRequest::default()
682        };
683
684        let request = BoundScanRequest::try_new(request, &dtype)?;
685
686        assert_eq!(request.projection.dtype(), &dtype);
687        assert!(request.filter.is_err());
688        Ok(())
689    }
690
691    struct StaticReaderFactory {
692        reader: LayoutReaderRef,
693    }
694
695    #[async_trait]
696    impl LayoutReaderFactory for StaticReaderFactory {
697        async fn open(&self) -> VortexResult<Option<LayoutReaderRef>> {
698            Ok(Some(Arc::clone(&self.reader)))
699        }
700    }
701
702    /// An unordered limit is shared by every file of the scan, so the files together return no
703    /// more than the limit even though each one is scanned independently.
704    #[test]
705    fn unordered_limit_is_shared_across_readers() -> VortexResult<()> {
706        let runtime = SingleThreadRuntime::default();
707        let session = session_with_handle(runtime.handle());
708        let first: LayoutReaderRef = Arc::new(TestLayoutReader::new(2));
709        let second: LayoutReaderRef = Arc::new(TestLayoutReader::new(2).with_base(10));
710        let source = MultiLayoutDataSource::new_with_first(
711            first,
712            vec![Arc::new(StaticReaderFactory { reader: second })],
713            vec![],
714            &session,
715        );
716
717        let scan = runtime.block_on(source.scan(ScanRequest {
718            filter: Some(root()),
719            limit: Some(3),
720            ordered: false,
721            ..Default::default()
722        }))?;
723        let partitions = runtime.block_on(scan.partitions().try_collect::<Vec<_>>())?;
724        assert_eq!(partitions.len(), 2);
725
726        let mut values = Vec::new();
727        for partition in partitions {
728            values.extend(collect_scan_values(
729                runtime.block_on_stream(partition.execute()?),
730            )?);
731        }
732
733        // Three of the four rows, whichever partition reserved them first.
734        assert_eq!(values.len(), 3);
735        Ok(())
736    }
737}