Skip to main content

vortex_layout/layouts/
table.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! A configurable writer strategy for tabular data.
5//!
6//! [`TableStrategy`] is a *dispatcher*: it inspects the dtype of the stream it is handed and
7//! routes struct columns to [`StructStrategy`], list columns to [`ListLayoutStrategy`], and
8//! everything else to the configured leaf strategy. Because it hands *itself* (suitably descended)
9//! to those structural writers as the strategy for their children, arbitrarily nested struct/list
10//! trees are written with no manual wiring.
11//!
12//! The dispatcher also owns field-path overrides, letting callers force a specific leaf field —
13//! at any depth — onto a custom strategy.
14
15use std::sync::Arc;
16
17use async_trait::async_trait;
18use vortex_array::dtype::Field;
19use vortex_array::dtype::FieldName;
20use vortex_array::dtype::FieldPath;
21use vortex_error::VortexResult;
22use vortex_session::VortexSession;
23use vortex_utils::aliases::hash_map::HashMap;
24use vortex_utils::aliases::hash_set::HashSet;
25
26use crate::LayoutRef;
27use crate::LayoutStrategy;
28use crate::LayoutWriterContext;
29use crate::layouts::list::writer::ListLayoutStrategy;
30use crate::layouts::struct_::StructStrategy;
31use crate::segments::SegmentSinkRef;
32use crate::sequence::SendableSequentialStream;
33use crate::sequence::SequencePointer;
34
35type ListLayoutFactory = Arc<dyn Fn(ListLayoutStrategy) -> Arc<dyn LayoutStrategy> + Send + Sync>;
36
37/// A configurable strategy for writing nested tabular data, dispatching each (sub)stream to the
38/// structural writer for its dtype.
39///
40/// Dispatch rules, applied to the dtype of the stream handed to [`write_stream`]:
41/// - **struct** → [`StructStrategy`], with each field written by its override (if any) or by a
42///   descended copy of this dispatcher.
43/// - **list** → [`ListLayoutStrategy`], with `elements` written by a descended copy of this
44///   dispatcher (so nested structs/lists recurse) and `offsets`/`validity` by the leaf/validity
45///   strategies. Gated: only when list decomposition is enabled via
46///   [`with_list_layout`][Self::with_list_layout] (off by default); otherwise a list falls through
47///   to the leaf strategy.
48/// - **anything else** → the leaf strategy.
49///
50/// [`write_stream`]: LayoutStrategy::write_stream
51pub struct TableStrategy {
52    /// A set of field-path overrides, e.g. to force one column to be compact-compressed. Keys are
53    /// paths relative to the level this dispatcher sits at.
54    leaf_writers: HashMap<FieldPath, Arc<dyn LayoutStrategy>>,
55    /// The writer for any validity arrays that may be present, at any level of the tree.
56    validity: Arc<dyn LayoutStrategy>,
57    /// The writer for leaf fields, i.e. anything that is not a struct.
58    leaf: Arc<dyn LayoutStrategy>,
59    /// Optional factory applied to each dynamically constructed [`ListLayoutStrategy`].
60    /// Its presence also enables list decomposition.
61    ///
62    /// [`ListLayoutStrategy`]: ListLayoutStrategy
63    list_layout_factory: Option<ListLayoutFactory>,
64}
65
66impl TableStrategy {
67    /// Create a new dispatcher with the given `validity` strategy and `fallback` leaf strategy and
68    /// no overrides.
69    ///
70    /// Additional per-field overrides can be configured with
71    /// [`with_field_writer`][Self::with_field_writer].
72    ///
73    /// ## Example
74    ///
75    /// ```ignore
76    /// # use std::sync::Arc;
77    /// # use vortex_layout::layouts::flat::writer::FlatLayoutStrategy;
78    /// # use vortex_layout::layouts::table::TableStrategy;
79    ///
80    /// // Build a write strategy that does not compress validity or any leaf fields.
81    /// let flat = Arc::new(FlatLayoutStrategy::default());
82    ///
83    /// let strategy = TableStrategy::new(Arc::<FlatLayoutStrategy>::clone(&flat), Arc::<FlatLayoutStrategy>::clone(&flat));
84    /// ```
85    pub fn new(validity: Arc<dyn LayoutStrategy>, fallback: Arc<dyn LayoutStrategy>) -> Self {
86        Self {
87            leaf_writers: Default::default(),
88            validity,
89            leaf: fallback,
90            list_layout_factory: None,
91        }
92    }
93
94    /// Add a custom write strategy for the given leaf field.
95    ///
96    /// ## Example
97    ///
98    /// ```ignore
99    /// # use std::sync::Arc;
100    /// # use vortex_array::dtype::{field_path, Field, FieldPath};
101    /// # use vortex_btrblocks::BtrBlocksCompressor;
102    /// # use vortex_layout::layouts::compressed::CompressingStrategy;
103    /// # use vortex_layout::layouts::flat::writer::FlatLayoutStrategy;
104    /// # use vortex_layout::layouts::table::TableStrategy;
105    ///
106    /// // A strategy for compressing data using the balanced BtrBlocks compressor.
107    /// let compress =
108    ///     CompressingStrategy::new(FlatLayoutStrategy::default(), BtrBlocksCompressor::from_session(&session));
109    ///
110    /// // Our combined strategy uses no compression for validity buffers, BtrBlocks compression
111    /// // for most columns, and stores a nested binary column uncompressed (flat) because it
112    /// // is pre-compressed or never filtered on.
113    /// let strategy = TableStrategy::new(
114    ///         Arc::new(FlatLayoutStrategy::default()),
115    ///         Arc::new(compress),
116    ///     )
117    ///     .with_field_writer(
118    ///         field_path!(request.body.bytes),
119    ///         Arc::new(FlatLayoutStrategy::default()),
120    ///     );
121    /// ```
122    pub fn with_field_writer(
123        mut self,
124        field_path: impl Into<FieldPath>,
125        writer: Arc<dyn LayoutStrategy>,
126    ) -> Self {
127        self.leaf_writers
128            .insert(self.validate_path(field_path.into()), writer);
129        self
130    }
131
132    /// Set writers for several fields at once.
133    ///
134    /// See also: [`with_field_writer`][Self::with_field_writer].
135    pub fn with_field_writers(
136        mut self,
137        writers: impl IntoIterator<Item = (FieldPath, Arc<dyn LayoutStrategy>)>,
138    ) -> Self {
139        for (field_path, strategy) in writers {
140            self.leaf_writers
141                .insert(self.validate_path(field_path), strategy);
142        }
143        self
144    }
145
146    /// Override the default strategy for leaf columns that don't have overrides.
147    pub fn with_default_strategy(mut self, default: Arc<dyn LayoutStrategy>) -> Self {
148        self.leaf = default;
149        self
150    }
151
152    /// Override the strategy for compressing struct validity at all levels of the schema tree.
153    pub fn with_validity_strategy(mut self, validity: Arc<dyn LayoutStrategy>) -> Self {
154        self.validity = validity;
155        self
156    }
157
158    /// Enable writing list fields with [`ListLayoutStrategy`].
159    ///
160    /// **Note**: this is an unstable and experimental layout that is expected to change.
161    /// Using it may lead to unreadable files in the future.
162    pub fn with_list_layout(self) -> Self {
163        self.with_list_layout_factory(|strategy| Arc::new(strategy))
164    }
165
166    /// Enable writing list fields with [`ListLayoutStrategy`] and wrap each list writer. This
167    /// allows repartitioning or zoning to operate in the list's outer-row space before shredding.
168    ///
169    /// **Note**: this is an unstable and experimental layout that is expected to change.
170    /// Using it may lead to unreadable files in the future.
171    pub fn with_list_layout_factory(
172        mut self,
173        factory: impl Fn(ListLayoutStrategy) -> Arc<dyn LayoutStrategy> + Send + Sync + 'static,
174    ) -> Self {
175        self.list_layout_factory = Some(Arc::new(factory));
176        self
177    }
178}
179
180impl TableStrategy {
181    /// Build the [`StructStrategy`] used to write a struct-typed stream at this level.
182    ///
183    /// Each field that has an override (or a deeper override beneath it) is resolved up front;
184    /// every other field falls through to a clean descended dispatcher.
185    fn struct_strategy(&self) -> StructStrategy {
186        let mut field_writers: HashMap<FieldName, Arc<dyn LayoutStrategy>> = HashMap::default();
187
188        // The distinct named first-segments of our override paths are the only fields that need
189        // anything other than the default dispatcher.
190        let mut named_first: HashSet<FieldName> = HashSet::default();
191        for path in self.leaf_writers.keys() {
192            if let Some(Field::Name(name)) = path.parts().first() {
193                named_first.insert(name.clone());
194            }
195        }
196
197        for name in named_first {
198            // `validate_path` forbids overlapping overrides, so a name has *either* an exact
199            // single-segment override *or* deeper overrides, never both.
200            let writer = match self.leaf_writers.get(&FieldPath::from_name(name.clone())) {
201                Some(exact) => Arc::clone(exact),
202                None => {
203                    Arc::new(self.descend(&Field::Name(name.clone()))) as Arc<dyn LayoutStrategy>
204                }
205            };
206            field_writers.insert(name, writer);
207        }
208
209        StructStrategy::new(Arc::clone(&self.validity), Arc::new(self.descend_clean()))
210            .with_field_writers(field_writers)
211    }
212
213    /// Build the [`ListLayoutStrategy`] used to write a list field stream at this level.
214    ///
215    /// The `elements` sub-column is routed back through a clean descended dispatcher so nested
216    /// structs/lists recurse; `offsets` go straight to the leaf (they are always a primitive
217    /// column); and `validity` uses the shared validity strategy.
218    fn list_strategy(&self) -> Option<Arc<dyn LayoutStrategy>> {
219        let factory = self.list_layout_factory.as_ref()?;
220        let list_layout = ListLayoutStrategy::default()
221            .with_elements(Arc::new(self.descend_clean()))
222            .with_offsets(Arc::clone(&self.leaf))
223            .with_validity(Arc::clone(&self.validity))
224            .with_fallback(Arc::clone(&self.leaf));
225        Some(factory(list_layout))
226    }
227
228    /// Descend into a subfield, retaining only the overrides that apply beneath it (rebased to be
229    /// relative to the child).
230    fn descend(&self, field: &Field) -> Self {
231        let mut new_writers = HashMap::with_capacity(self.leaf_writers.len());
232
233        for (field_path, strategy) in &self.leaf_writers {
234            if field_path.parts().first() == Some(field)
235                && let Some(subpath) = field_path.clone().step_into()
236                && !subpath.is_root()
237            {
238                new_writers.insert(subpath, Arc::clone(strategy));
239            }
240        }
241
242        Self {
243            leaf_writers: new_writers,
244            validity: Arc::clone(&self.validity),
245            leaf: Arc::clone(&self.leaf),
246            list_layout_factory: self.list_layout_factory.clone(),
247        }
248    }
249
250    /// A copy of this dispatcher with no overrides, used as the default child strategy for fields
251    /// that carry no override.
252    fn descend_clean(&self) -> Self {
253        Self {
254            leaf_writers: HashMap::default(),
255            validity: Arc::clone(&self.validity),
256            leaf: Arc::clone(&self.leaf),
257            list_layout_factory: self.list_layout_factory.clone(),
258        }
259    }
260
261    fn validate_path(&self, path: FieldPath) -> FieldPath {
262        assert!(
263            !path.is_root(),
264            "Do not set override as a root strategy, instead set the default strategy"
265        );
266
267        // Validate that the field path does not conflict with any overrides
268        // that we've added by overlapping.
269        for field_path in self.leaf_writers.keys() {
270            assert!(
271                !path.overlap(field_path),
272                "Override for field_path {path} conflicts with existing override for {field_path}"
273            );
274        }
275
276        path
277    }
278}
279
280/// Dispatches each stream to the structural writer for its dtype.
281#[async_trait]
282impl LayoutStrategy for TableStrategy {
283    async fn write_stream(
284        &self,
285        ctx: LayoutWriterContext,
286        segment_sink: SegmentSinkRef,
287        stream: SendableSequentialStream,
288        eof: SequencePointer,
289        session: &VortexSession,
290    ) -> VortexResult<LayoutRef> {
291        let dtype = stream.dtype().clone();
292
293        if dtype.is_struct() {
294            return self
295                .struct_strategy()
296                .write_stream(ctx, segment_sink, stream, eof, session)
297                .await;
298        }
299
300        if dtype.is_list()
301            && let Some(list_strategy) = self.list_strategy()
302        {
303            return list_strategy
304                .write_stream(ctx, segment_sink, stream, eof, session)
305                .await;
306        }
307
308        // Leaf: hand off to the leaf strategy.
309        self.leaf
310            .write_stream(ctx, segment_sink, stream, eof, session)
311            .await
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use std::num::NonZeroUsize;
318    use std::sync::Arc;
319    use std::task::Poll;
320
321    use vortex_array::ArrayContext;
322    use vortex_array::ArrayRef;
323    use vortex_array::IntoArray;
324    use vortex_array::arrays::BoolArray;
325    use vortex_array::arrays::ChunkedArray;
326    use vortex_array::arrays::ListArray;
327    use vortex_array::arrays::PrimitiveArray;
328    use vortex_array::arrays::StructArray;
329    use vortex_array::dtype::DType;
330    use vortex_array::dtype::FieldPath;
331    use vortex_array::dtype::Nullability;
332    use vortex_array::dtype::PType;
333    use vortex_array::dtype::StructFields;
334    use vortex_array::field_path;
335    use vortex_array::validity::Validity;
336    use vortex_buffer::buffer;
337    use vortex_error::VortexExpect;
338    use vortex_error::VortexResult;
339    use vortex_io::session::RuntimeSessionExt;
340
341    use crate::LayoutRef;
342    use crate::LayoutStrategy;
343    use crate::layouts::chunked::Chunked;
344    use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
345    use crate::layouts::flat::Flat;
346    use crate::layouts::flat::writer::FlatLayoutStrategy;
347    use crate::layouts::list::List;
348    use crate::layouts::repartition::RepartitionStrategy;
349    use crate::layouts::repartition::RepartitionWriterOptions;
350    use crate::layouts::table::TableStrategy;
351    use crate::layouts::zoned::Zoned;
352    use crate::layouts::zoned::writer::ZonedLayoutOptions;
353    use crate::layouts::zoned::writer::ZonedStrategy;
354    use crate::segments::TestSegments;
355    use crate::sequence::SequenceId;
356    use crate::sequence::SequentialArrayStreamExt;
357    use crate::sequence::SequentialStreamAdapter;
358    use crate::sequence::SequentialStreamExt;
359    use crate::test::SESSION;
360    use crate::test::new_session;
361
362    async fn write<S: LayoutStrategy>(strategy: &S, array: ArrayRef) -> VortexResult<LayoutRef> {
363        let segments = Arc::new(TestSegments::default());
364        let (ptr, eof) = SequenceId::root().split();
365        let stream = array.to_array_stream().sequenced(ptr);
366        let session = new_session().with_tokio();
367        strategy
368            .write_stream(
369                ArrayContext::empty().into(),
370                segments,
371                stream,
372                eof,
373                &session,
374            )
375            .await
376    }
377
378    /// A plain table dispatcher with no overrides. `flat` here is both the validity and leaf
379    /// strategy.
380    fn flat_table() -> TableStrategy {
381        let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
382        TableStrategy::new(Arc::clone(&flat), flat)
383    }
384
385    /// The dispatcher shreds a top-level struct into one child per field.
386    #[tokio::test]
387    async fn dispatches_struct() -> VortexResult<()> {
388        let struct_array = StructArray::from_fields(
389            [
390                ("a", buffer![1i32, 2, 3].into_array()),
391                ("b", buffer![10i32, 20, 30].into_array()),
392            ]
393            .as_slice(),
394        )?
395        .into_array();
396
397        let layout = write(&flat_table(), struct_array).await?;
398        insta::assert_snapshot!(layout.display_tree(), @r"
399        vortex.struct, dtype: {a=i32, b=i32}, children: 2
400        ├── a: vortex.flat, dtype: i32, segment: 0
401        └── b: vortex.flat, dtype: i32, segment: 1
402        ");
403        Ok(())
404    }
405
406    /// A `list<list<i32>>` column: the dispatcher recurses into itself so the outer list's
407    /// `elements` are decomposed as a nested `ListLayout`.
408    #[tokio::test]
409    async fn dispatches_nested_list() -> VortexResult<()> {
410        let inner = ListArray::try_new(
411            buffer![1i32, 2, 3, 4, 5, 6].into_array(),
412            buffer![0u32, 2, 5, 5, 6].into_array(),
413            Validity::NonNullable,
414        )?
415        .into_array();
416        let outer = ListArray::try_new(
417            inner,
418            buffer![0u32, 2, 4].into_array(),
419            Validity::NonNullable,
420        )?
421        .into_array();
422
423        let layout = write(&flat_table().with_list_layout(), outer).await?;
424        insta::assert_snapshot!(layout.display_tree(), @r"
425        vortex.list, dtype: list(list(i32)), children: 2
426        ├── elements: vortex.list, dtype: list(i32), children: 2
427        │   ├── elements: vortex.flat, dtype: i32, segment: 1
428        │   └── offsets: vortex.flat, dtype: u64, segment: 2
429        └── offsets: vortex.flat, dtype: u64, segment: 0
430        ");
431        Ok(())
432    }
433
434    /// A `struct<{ items: list<struct<{a,b}>>? }>` column: list decomposition recurses into struct
435    /// decomposition for the elements, and a nullable list writes a validity child.
436    #[tokio::test]
437    async fn dispatches_struct_list_struct() -> VortexResult<()> {
438        let inner_struct = StructArray::from_fields(
439            [
440                ("a", buffer![1i32, 2, 3, 4, 5].into_array()),
441                ("b", buffer![10i32, 20, 30, 40, 50].into_array()),
442            ]
443            .as_slice(),
444        )?
445        .into_array();
446        let items = ListArray::try_new(
447            inner_struct,
448            buffer![0u32, 2, 5, 5].into_array(),
449            Validity::Array(BoolArray::from_iter([true, false, true]).into_array()),
450        )?
451        .into_array();
452        let st = StructArray::from_fields([("items", items)].as_slice())?.into_array();
453
454        let layout = write(&flat_table().with_list_layout(), st).await?;
455        insta::assert_snapshot!(layout.display_tree(), @r"
456        vortex.struct, dtype: {items=list({a=i32, b=i32})?}, children: 1
457        └── items: vortex.list, dtype: list({a=i32, b=i32})?, children: 3
458            ├── elements: vortex.struct, dtype: {a=i32, b=i32}, children: 2
459            │   ├── a: vortex.flat, dtype: i32, segment: 2
460            │   └── b: vortex.flat, dtype: i32, segment: 3
461            ├── offsets: vortex.flat, dtype: u64, segment: 0
462            └── validity: vortex.flat, dtype: bool, segment: 1
463        ");
464        Ok(())
465    }
466
467    /// A multi-chunk `list<i32>` written with a chunked leaf: each sub-column (`elements`,
468    /// `offsets`) becomes its own `ChunkedLayout`, so elements are chunked independently of rows.
469    /// This is the "list-of-chunkeds" topology top-level decomposition unlocks.
470    #[tokio::test]
471    async fn dispatches_chunked_list() -> VortexResult<()> {
472        let chunk0 = ListArray::try_new(
473            buffer![1i32, 2, 3].into_array(),
474            buffer![0u32, 2, 3].into_array(),
475            Validity::NonNullable,
476        )?
477        .into_array();
478        let chunk1 = ListArray::try_new(
479            buffer![4i32, 5, 6, 7].into_array(),
480            buffer![0u32, 1, 4].into_array(),
481            Validity::NonNullable,
482        )?
483        .into_array();
484        let dtype = chunk0.dtype().clone();
485        let chunked = ChunkedArray::try_new(vec![chunk0, chunk1], dtype)?.into_array();
486
487        let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
488        let dispatcher = TableStrategy::new(
489            Arc::clone(&flat),
490            Arc::new(ChunkedLayoutStrategy::new(FlatLayoutStrategy::default())),
491        )
492        .with_list_layout();
493        let layout = write(&dispatcher, chunked).await?;
494        insta::assert_snapshot!(layout.display_tree(), @r"
495        vortex.list, dtype: list(i32), children: 2
496        ├── elements: vortex.chunked, dtype: i32, children: 2
497        │   ├── [0]: vortex.flat, dtype: i32, segment: 0
498        │   └── [1]: vortex.flat, dtype: i32, segment: 1
499        └── offsets: vortex.chunked, dtype: u64, children: 2
500            ├── [0]: vortex.flat, dtype: u64, segment: 2
501            └── [1]: vortex.flat, dtype: u64, segment: 3
502        ");
503        Ok(())
504    }
505
506    /// A wrapper can repartition and zone lists in outer-row space before decomposition.
507    #[tokio::test]
508    async fn wraps_list_strategy_before_decomposition() -> VortexResult<()> {
509        let list = ListArray::try_new(
510            PrimitiveArray::from_iter(0..9_i32).into_array(),
511            PrimitiveArray::from_iter(0..=9_u32).into_array(),
512            Validity::NonNullable,
513        )?
514        .into_array();
515
516        let row_block_size = NonZeroUsize::new(4).vortex_expect("4 is non-zero");
517        let flat: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
518        let stats = Arc::clone(&flat);
519        let chunked: Arc<dyn LayoutStrategy> =
520            Arc::new(ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()));
521        let dispatcher = TableStrategy::new(Arc::clone(&flat), chunked).with_list_layout_factory(
522            move |list_layout| {
523                let zoned = ZonedStrategy::new(
524                    list_layout,
525                    Arc::clone(&stats),
526                    ZonedLayoutOptions {
527                        block_size: row_block_size,
528                        ..Default::default()
529                    },
530                );
531                Arc::new(RepartitionStrategy::new(
532                    zoned,
533                    RepartitionWriterOptions {
534                        block_size_minimum: 0,
535                        block_len_multiple: row_block_size.get(),
536                        block_size_target: None,
537                        canonicalize: false,
538                    },
539                )) as Arc<dyn LayoutStrategy>
540            },
541        );
542
543        let layout = write(&dispatcher, list).await?;
544        let zoned = layout.as_::<Zoned>();
545        assert_eq!(zoned.zone_len(), 4);
546        assert_eq!(zoned.nzones(), 3);
547
548        let data = layout
549            .slot(0)?
550            .vortex_expect("ZonedLayout always has a data child");
551        assert!(data.is::<List>());
552        assert_eq!(data.row_count(), 9);
553        Ok(())
554    }
555
556    /// A non-struct stream is not shredded; it is handed straight to the leaf strategy.
557    #[tokio::test]
558    async fn non_struct_input_uses_leaf() -> VortexResult<()> {
559        let primitive = PrimitiveArray::from_iter([1i32, 2, 3]).into_array();
560        let layout = write(&flat_table(), primitive).await?;
561        insta::assert_snapshot!(layout.display_tree(), @"vortex.flat, dtype: i32, segment: 0");
562        Ok(())
563    }
564
565    /// A multi-chunk struct is transposed per field; each column is written by a chunked leaf.
566    #[tokio::test]
567    async fn chunked_struct() -> VortexResult<()> {
568        let validity: Arc<dyn LayoutStrategy> = Arc::new(FlatLayoutStrategy::default());
569        let chunked_flat: Arc<dyn LayoutStrategy> =
570            Arc::new(ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()));
571        let dispatcher = TableStrategy::new(validity, chunked_flat);
572
573        let c0 = StructArray::from_fields(
574            [
575                ("a", buffer![1i32, 2].into_array()),
576                ("b", buffer![10i32, 20].into_array()),
577            ]
578            .as_slice(),
579        )?
580        .into_array();
581        let c1 = StructArray::from_fields(
582            [
583                ("a", buffer![3i32].into_array()),
584                ("b", buffer![30i32].into_array()),
585            ]
586            .as_slice(),
587        )?
588        .into_array();
589        let dtype = c0.dtype().clone();
590        let chunked = ChunkedArray::try_new(vec![c0, c1], dtype)?.into_array();
591
592        let layout = write(&dispatcher, chunked).await?;
593        insta::assert_snapshot!(layout.display_tree(), @r"
594        vortex.struct, dtype: {a=i32, b=i32}, children: 2
595        ├── a: vortex.chunked, dtype: i32, children: 2
596        │   ├── [0]: vortex.flat, dtype: i32, segment: 0
597        │   └── [1]: vortex.flat, dtype: i32, segment: 1
598        └── b: vortex.chunked, dtype: i32, children: 2
599            ├── [0]: vortex.flat, dtype: i32, segment: 2
600            └── [1]: vortex.flat, dtype: i32, segment: 3
601        ");
602        Ok(())
603    }
604
605    /// A field override on a struct field is honored ahead of the default leaf strategy.
606    #[tokio::test]
607    async fn field_override_is_used() -> VortexResult<()> {
608        let struct_array = StructArray::from_fields(
609            [
610                ("a", buffer![1i32, 2, 3].into_array()),
611                ("b", buffer![10i32, 20, 30].into_array()),
612            ]
613            .as_slice(),
614        )?
615        .into_array();
616
617        let strategy = flat_table().with_field_writer(
618            field_path!(a),
619            Arc::new(RepartitionStrategy::new(
620                ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()),
621                RepartitionWriterOptions {
622                    block_size_minimum: 0,
623                    block_len_multiple: 2,
624                    block_size_target: None,
625                    canonicalize: false,
626                },
627            )),
628        );
629        let layout = write(&strategy, struct_array).await?;
630        assert_eq!(layout.nchildren(), 2);
631        let overridden = layout.slot(1)?.vortex_expect("field a");
632        assert!(overridden.is::<Chunked>());
633        assert_eq!(overridden.nchildren(), 2);
634        assert_eq!(
635            overridden.slot(0)?.vortex_expect("first chunk").row_count(),
636            2
637        );
638        assert_eq!(
639            overridden
640                .slot(1)?
641                .vortex_expect("second chunk")
642                .row_count(),
643            1
644        );
645        assert!(layout.slot(2)?.vortex_expect("field b").is::<Flat>());
646        Ok(())
647    }
648
649    #[test]
650    #[should_panic(
651        expected = "Override for field_path $a.$b conflicts with existing override for $a.$b.$c"
652    )]
653    fn test_overlapping_paths_fail() {
654        let flat = Arc::new(FlatLayoutStrategy::default());
655
656        // Success
657        let path = TableStrategy::new(
658            Arc::<FlatLayoutStrategy>::clone(&flat),
659            Arc::<FlatLayoutStrategy>::clone(&flat),
660        )
661        .with_field_writer(field_path!(a.b.c), Arc::<FlatLayoutStrategy>::clone(&flat));
662
663        // Should panic right here.
664        let _path = path.with_field_writer(field_path!(a.b), flat);
665    }
666
667    #[test]
668    #[should_panic(
669        expected = "Do not set override as a root strategy, instead set the default strategy"
670    )]
671    fn test_root_override() {
672        let flat = Arc::new(FlatLayoutStrategy::default());
673        let _strategy = TableStrategy::new(
674            Arc::<FlatLayoutStrategy>::clone(&flat),
675            Arc::<FlatLayoutStrategy>::clone(&flat),
676        )
677        .with_field_writer(FieldPath::root(), flat);
678    }
679
680    #[tokio::test]
681    #[should_panic(expected = "panic while transposing table stream")]
682    async fn table_fanout_panic_propagates() {
683        let ctx = ArrayContext::empty();
684        let segments = Arc::new(TestSegments::default());
685        let (_, eof) = SequenceId::root().split();
686        let dtype = DType::Struct(
687            StructFields::from_iter([(
688                "a",
689                DType::Primitive(PType::I32, Nullability::NonNullable),
690            )]),
691            Nullability::NonNullable,
692        );
693        let stream =
694            futures::stream::poll_fn(|_| -> Poll<Option<VortexResult<(SequenceId, ArrayRef)>>> {
695                panic!("panic while transposing table stream");
696            });
697        let strategy = TableStrategy::new(
698            Arc::new(FlatLayoutStrategy::default()),
699            Arc::new(FlatLayoutStrategy::default()),
700        );
701
702        strategy
703            .write_stream(
704                ctx.into(),
705                segments,
706                SequentialStreamAdapter::new(dtype, stream).sendable(),
707                eof,
708                &SESSION,
709            )
710            .await
711            .unwrap();
712    }
713}