Skip to main content

vortex_layout/layouts/flat/
writer.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use async_trait::async_trait;
5use futures::StreamExt;
6use vortex_array::ArrayContext;
7use vortex_array::dtype::DType;
8use vortex_array::expr::stats::Precision;
9use vortex_array::expr::stats::Stat;
10use vortex_array::expr::stats::StatsProvider;
11use vortex_array::scalar::Scalar;
12use vortex_array::scalar::ScalarTruncation;
13use vortex_array::scalar::lower_bound;
14use vortex_array::scalar::upper_bound;
15use vortex_array::serde::SerializeOptions;
16use vortex_array::stats::StatsSetRef;
17use vortex_buffer::BufferString;
18use vortex_buffer::ByteBuffer;
19use vortex_error::VortexExpect;
20use vortex_error::VortexResult;
21use vortex_error::vortex_bail;
22use vortex_session::VortexSession;
23use vortex_session::registry::ReadContext;
24
25use crate::LayoutRef;
26use crate::LayoutStrategy;
27use crate::children::OwnedLayoutChildren;
28use crate::layouts::chunked::ChunkedLayout;
29use crate::layouts::flat::FlatLayout;
30use crate::layouts::flat::flat_layout_inline_array_node;
31use crate::segments::SegmentSinkRef;
32use crate::sequence::SendableSequentialStream;
33use crate::sequence::SequencePointer;
34
35#[derive(Clone)]
36pub struct FlatLayoutStrategy {
37    /// Whether to include padding for memory-mapped reads.
38    pub include_padding: bool,
39    /// Maximum length of variable length statistics
40    pub max_variable_length_statistics_size: usize,
41}
42
43impl Default for FlatLayoutStrategy {
44    fn default() -> Self {
45        Self {
46            include_padding: true,
47            max_variable_length_statistics_size: 64,
48        }
49    }
50}
51
52impl FlatLayoutStrategy {
53    /// Set whether to include padding for memory-mapped reads.
54    pub fn with_include_padding(mut self, include_padding: bool) -> Self {
55        self.include_padding = include_padding;
56        self
57    }
58
59    /// Set the maximum length of variable length statistics.
60    pub fn with_max_variable_length_statistics_size(mut self, size: usize) -> Self {
61        self.max_variable_length_statistics_size = size;
62        self
63    }
64}
65
66fn truncate_scalar_stat<F: Fn(Scalar) -> Option<(Scalar, bool)>>(
67    statistics: StatsSetRef<'_>,
68    stat: Stat,
69    truncation: F,
70) {
71    if let Some(sv) = statistics.get(stat).into_inner() {
72        if let Some((truncated_value, truncated)) = truncation(sv) {
73            if truncated && let Some(v) = truncated_value.into_value() {
74                statistics.set(stat, Precision::Inexact(v));
75            }
76        } else {
77            statistics.clear(stat)
78        }
79    }
80}
81
82#[async_trait]
83impl LayoutStrategy for FlatLayoutStrategy {
84    async fn write_stream(
85        &self,
86        ctx: ArrayContext,
87        segment_sink: SegmentSinkRef,
88        mut stream: SendableSequentialStream,
89        _eof: SequencePointer,
90        session: &VortexSession,
91    ) -> VortexResult<LayoutRef> {
92        let ctx = ctx.clone();
93        let Some(chunk) = stream.next().await else {
94            // an empty input has no segment to write.
95            return Ok(ChunkedLayout::new(
96                0,
97                stream.dtype().clone(),
98                OwnedLayoutChildren::layout_children(vec![]),
99            )
100            .into_layout());
101        };
102        let (sequence_id, chunk) = chunk?;
103
104        let row_count = chunk.len() as u64;
105
106        match chunk.dtype() {
107            DType::Utf8(n) => {
108                truncate_scalar_stat(chunk.statistics(), Stat::Min, |v| {
109                    lower_bound(
110                        BufferString::from_scalar(v)
111                            .vortex_expect("utf8 scalar must be a BufferString"),
112                        self.max_variable_length_statistics_size,
113                        *n,
114                    )
115                });
116                truncate_scalar_stat(chunk.statistics(), Stat::Max, |v| {
117                    upper_bound(
118                        BufferString::from_scalar(v)
119                            .vortex_expect("utf8 scalar must be a BufferString"),
120                        self.max_variable_length_statistics_size,
121                        *n,
122                    )
123                });
124            }
125            DType::Binary(n) => {
126                truncate_scalar_stat(chunk.statistics(), Stat::Min, |v| {
127                    lower_bound(
128                        ByteBuffer::from_scalar(v)
129                            .vortex_expect("binary scalar must be a ByteBuffer"),
130                        self.max_variable_length_statistics_size,
131                        *n,
132                    )
133                });
134                truncate_scalar_stat(chunk.statistics(), Stat::Max, |v| {
135                    upper_bound(
136                        ByteBuffer::from_scalar(v)
137                            .vortex_expect("binary scalar must be a ByteBuffer"),
138                        self.max_variable_length_statistics_size,
139                        *n,
140                    )
141                });
142            }
143            _ => {}
144        }
145
146        let buffers = chunk.serialize(
147            &ctx,
148            session,
149            &SerializeOptions {
150                offset: 0,
151                include_padding: self.include_padding,
152            },
153        )?;
154        // there is at least the flatbuffer and the length
155        assert!(buffers.len() >= 2);
156        let array_node =
157            flat_layout_inline_array_node().then(|| buffers[buffers.len() - 2].clone());
158        let segment_id = segment_sink.write(sequence_id, buffers).await?;
159
160        let None = stream.next().await else {
161            vortex_bail!("flat layout received stream with more than a single chunk");
162        };
163        Ok(FlatLayout::new_with_metadata(
164            row_count,
165            stream.dtype().clone(),
166            segment_id,
167            ReadContext::new(ctx.to_ids()),
168            array_node,
169        )
170        .into_layout())
171    }
172
173    fn buffered_bytes(&self) -> u64 {
174        // FlatLayoutStrategy is a leaf strategy with no child strategies and no buffering
175        0
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use std::sync::Arc;
182
183    use vortex_array::ArrayContext;
184    use vortex_array::ArrayRef;
185    use vortex_array::IntoArray;
186    use vortex_array::MaskFuture;
187    use vortex_array::VortexSessionExecute;
188    use vortex_array::array_session;
189    use vortex_array::arrays::BoolArray;
190    use vortex_array::arrays::Dict;
191    use vortex_array::arrays::DictArray;
192    use vortex_array::arrays::PrimitiveArray;
193    use vortex_array::arrays::StructArray;
194    use vortex_array::arrays::struct_::StructArrayExt;
195    use vortex_array::builders::ArrayBuilder;
196    use vortex_array::builders::VarBinViewBuilder;
197    use vortex_array::dtype::DType;
198    use vortex_array::dtype::FieldName;
199    use vortex_array::dtype::FieldNames;
200    use vortex_array::dtype::Nullability;
201    use vortex_array::expr::root;
202    use vortex_array::expr::stats::Precision;
203    use vortex_array::expr::stats::Stat;
204    use vortex_array::expr::stats::StatsProviderExt;
205    use vortex_array::validity::Validity;
206    use vortex_array::vtable::VTable;
207    use vortex_buffer::BitBufferMut;
208    use vortex_buffer::buffer;
209    use vortex_error::VortexExpect;
210    use vortex_error::VortexResult;
211    use vortex_io::runtime::single::block_on;
212    use vortex_io::session::RuntimeSessionExt;
213    use vortex_mask::AllOr;
214    use vortex_mask::Mask;
215    use vortex_utils::aliases::hash_set::HashSet;
216
217    use crate::LayoutStrategy;
218    use crate::LayoutStrategyEncodingValidator;
219    use crate::layouts::flat::writer::FlatLayoutStrategy;
220    use crate::segments::TestSegments;
221    use crate::sequence::SequenceId;
222    use crate::sequence::SequentialArrayStreamExt;
223    use crate::test::SESSION;
224    use crate::test::new_session;
225
226    // Currently, flat layouts do not force compute stats during write, they only retain
227    // pre-computed stats.
228    #[should_panic]
229    #[test]
230    fn flat_stats() {
231        block_on(|handle| async {
232            let session = new_session().with_handle(handle);
233            let ctx = ArrayContext::empty();
234            let segments = Arc::new(TestSegments::default());
235            let (ptr, eof) = SequenceId::root().split();
236            let array = PrimitiveArray::new(buffer![1, 2, 3, 4, 5], Validity::AllValid);
237            let layout = FlatLayoutStrategy::default()
238                .write_stream(
239                    ctx,
240                    Arc::<TestSegments>::clone(&segments),
241                    array.into_array().to_array_stream().sequenced(ptr),
242                    eof,
243                    &session,
244                )
245                .await
246                .unwrap();
247
248            let result = layout
249                .new_reader("".into(), segments, &SESSION, &Default::default())
250                .unwrap()
251                .projection_evaluation(
252                    &(0..layout.row_count()),
253                    &root(),
254                    MaskFuture::new_true(layout.row_count().try_into().unwrap()),
255                )
256                .unwrap()
257                .await
258                .unwrap();
259
260            assert_eq!(
261                result.statistics().get_as::<bool>(Stat::IsSorted),
262                Precision::Exact(true)
263            );
264        })
265    }
266
267    #[test]
268    fn truncates_variable_size_stats() {
269        block_on(|handle| async {
270            let session = new_session().with_handle(handle);
271            let ctx = ArrayContext::empty();
272            let segments = Arc::new(TestSegments::default());
273            let (ptr, eof) = SequenceId::root().split();
274            let mut builder =
275                VarBinViewBuilder::with_capacity(DType::Utf8(Nullability::NonNullable), 2);
276            builder.append_value("Long value to test that the statistics are actually truncated, it needs a bit of extra padding though");
277            builder.append_value("Another string that's meant to be smaller than the previous value, though still need extra padding");
278            let array = builder.finish();
279            let mut stats_ctx = session.create_execution_ctx();
280            array.statistics().set_iter(
281                array
282                    .statistics()
283                    .compute_all(&Stat::all().collect::<Vec<_>>(), &mut stats_ctx)
284                    .vortex_expect("stats computation should succeed for test array")
285                    .into_iter(),
286            );
287
288            let layout = FlatLayoutStrategy::default()
289                .write_stream(
290                    ctx,
291                    Arc::<TestSegments>::clone(&segments),
292                    array.into_array().to_array_stream().sequenced(ptr),
293                    eof,
294                    &session,
295                )
296                .await
297                .unwrap();
298
299            let result = layout
300                .new_reader("".into(), segments, &SESSION, &Default::default())
301                .unwrap()
302                .projection_evaluation(
303                    &(0..layout.row_count()),
304                    &root(),
305                    MaskFuture::new_true(layout.row_count().try_into().unwrap()),
306                )
307                .unwrap()
308                .await
309                .unwrap();
310
311            assert_eq!(
312                result.statistics().get_as::<String>(Stat::Min),
313                // The typo is correct, we need this to be truncated.
314                Precision::Inexact(
315                    // spellchecker:ignore-next-line
316                    "Another string that's meant to be smaller than the previous valu".to_string()
317                )
318            );
319            assert_eq!(
320                result.statistics().get_as::<String>(Stat::Max),
321                Precision::Inexact(
322                    "Long value to test that the statistics are actually truncated, j".to_string()
323                )
324            );
325        })
326    }
327
328    #[test]
329    fn struct_array_round_trip() {
330        block_on(|handle| async {
331            let mut ctx_exec = array_session().create_execution_ctx();
332            let session = new_session().with_handle(handle);
333            let mut validity_builder = BitBufferMut::with_capacity(2);
334            validity_builder.append(true);
335            validity_builder.append(false);
336            let validity_boolean_buffer = validity_builder.freeze();
337            let validity = Validity::Array(
338                BoolArray::new(validity_boolean_buffer.clone(), Validity::NonNullable).into_array(),
339            );
340            let array = StructArray::try_new(
341                FieldNames::from([FieldName::from("a"), FieldName::from("b")]),
342                vec![
343                    buffer![1_u64, 2].into_array(),
344                    buffer![3_u64, 4].into_array(),
345                ],
346                2,
347                validity,
348            )
349            .unwrap();
350
351            let ctx = ArrayContext::empty();
352
353            // Write the array into a byte buffer.
354            let (layout, segments) = {
355                let segments = Arc::new(TestSegments::default());
356                let (ptr, eof) = SequenceId::root().split();
357                let layout = FlatLayoutStrategy::default()
358                    .write_stream(
359                        ctx,
360                        Arc::<TestSegments>::clone(&segments),
361                        array.into_array().to_array_stream().sequenced(ptr),
362                        eof,
363                        &session,
364                    )
365                    .await
366                    .unwrap();
367
368                (layout, segments)
369            };
370
371            // We should be able to read the array we just wrote.
372            let result: ArrayRef = layout
373                .new_reader("".into(), segments, &SESSION, &Default::default())
374                .unwrap()
375                .projection_evaluation(
376                    &(0..layout.row_count()),
377                    &root(),
378                    MaskFuture::new_true(layout.row_count().try_into().unwrap()),
379                )
380                .unwrap()
381                .await
382                .unwrap();
383
384            assert_eq!(
385                result
386                    .validity()
387                    .unwrap()
388                    .execute_mask(result.len(), &mut ctx_exec)
389                    .unwrap()
390                    .bit_buffer(),
391                AllOr::Some(&validity_boolean_buffer)
392            );
393            let result_struct = result
394                .clone()
395                .execute::<StructArray>(&mut ctx_exec)
396                .unwrap();
397            let field_a = result_struct
398                .unmasked_field_by_name("a")
399                .unwrap()
400                .clone()
401                .execute::<PrimitiveArray>(&mut ctx_exec)
402                .unwrap();
403            assert_eq!(field_a.as_slice::<u64>(), &[1, 2]);
404            let result_struct_b = result.execute::<StructArray>(&mut ctx_exec).unwrap();
405            let field_b = result_struct_b
406                .unmasked_field_by_name("b")
407                .unwrap()
408                .clone()
409                .execute::<PrimitiveArray>(&mut ctx_exec)
410                .unwrap();
411            assert_eq!(field_b.as_slice::<u64>(), &[3, 4]);
412        })
413    }
414
415    #[test]
416    fn flat_invalid_array_fails() -> VortexResult<()> {
417        block_on(|handle| async {
418            let session = new_session().with_handle(handle);
419            let prim: PrimitiveArray = (0..10).collect();
420            let filter = prim.filter(Mask::from_indices(10, vec![2, 3]))?;
421
422            let ctx = ArrayContext::empty();
423
424            // Write the array into a byte buffer.
425            let (layout, _segments) = {
426                let segments = Arc::new(TestSegments::default());
427                let (ptr, eof) = SequenceId::root().split();
428                // Disallow all encodings so filter arrays fail normalization immediately.
429                let allowed = HashSet::default();
430                let layout =
431                    LayoutStrategyEncodingValidator::new(FlatLayoutStrategy::default(), allowed)
432                        .write_stream(
433                            ctx,
434                            Arc::<TestSegments>::clone(&segments),
435                            filter.into_array().to_array_stream().sequenced(ptr),
436                            eof,
437                            &session,
438                        )
439                        .await;
440
441                (layout, segments)
442            };
443
444            let err = layout.expect_err("expected error");
445            assert!(
446                err.to_string()
447                    .contains("normalize forbids encoding (vortex.filter)"),
448                "unexpected error: {err}"
449            );
450
451            Ok(())
452        })
453    }
454
455    #[test]
456    fn flat_valid_array_writes() -> VortexResult<()> {
457        block_on(|handle| async {
458            let session = new_session().with_handle(handle);
459            let codes: PrimitiveArray = (0u32..10).collect();
460            let values: PrimitiveArray = (0..10).collect();
461            let dict = DictArray::new(codes.into_array(), values.into_array());
462
463            let ctx = ArrayContext::empty();
464
465            // Write the array into a byte buffer.
466            let (layout, _segments) = {
467                let segments = Arc::new(TestSegments::default());
468                let (ptr, eof) = SequenceId::root().split();
469                // Only allow the dict encoding; canonical primitive children remain permitted.
470                let mut allowed = HashSet::default();
471                allowed.insert(Dict.id());
472                let layout =
473                    LayoutStrategyEncodingValidator::new(FlatLayoutStrategy::default(), allowed)
474                        .write_stream(
475                            ctx,
476                            Arc::<TestSegments>::clone(&segments),
477                            dict.into_array().to_array_stream().sequenced(ptr),
478                            eof,
479                            &session,
480                        )
481                        .await;
482
483                (layout, segments)
484            };
485
486            assert!(layout.is_ok());
487
488            Ok(())
489        })
490    }
491}