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