vortex_layout/layouts/flat/
writer.rs1use 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 pub include_padding: bool,
39 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 pub fn with_include_padding(mut self, include_padding: bool) -> Self {
55 self.include_padding = include_padding;
56 self
57 }
58
59 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 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 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 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 #[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 Precision::Inexact(
315 "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 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 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 let (layout, _segments) = {
426 let segments = Arc::new(TestSegments::default());
427 let (ptr, eof) = SequenceId::root().split();
428 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 let (layout, _segments) = {
467 let segments = Arc::new(TestSegments::default());
468 let (ptr, eof) = SequenceId::root().split();
469 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}