vortex_layout/layouts/flat/
writer.rs1use async_trait::async_trait;
5use futures::StreamExt;
6use vortex_array::dtype::DType;
7use vortex_array::expr::stats::Precision;
8use vortex_array::expr::stats::Stat;
9use vortex_array::expr::stats::StatsProvider;
10use vortex_array::scalar::Scalar;
11use vortex_array::scalar::ScalarTruncation;
12use vortex_array::scalar::lower_bound;
13use vortex_array::scalar::upper_bound;
14use vortex_array::serde::SerializeOptions;
15use vortex_array::stats::StatsSetRef;
16use vortex_buffer::BufferString;
17use vortex_buffer::ByteBuffer;
18use vortex_error::VortexExpect;
19use vortex_error::VortexResult;
20use vortex_error::vortex_bail;
21use vortex_session::VortexSession;
22use vortex_session::registry::ReadContext;
23
24use crate::LayoutRef;
25use crate::LayoutStrategy;
26use crate::LayoutWriterContext;
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: LayoutWriterContext,
87 segment_sink: SegmentSinkRef,
88 mut stream: SendableSequentialStream,
89 _eof: SequencePointer,
90 session: &VortexSession,
91 ) -> VortexResult<LayoutRef> {
92 let Some(chunk) = stream.next().await else {
93 return Ok(ChunkedLayout::new(
95 0,
96 stream.dtype().clone(),
97 OwnedLayoutChildren::layout_children(vec![]),
98 )
99 .into_layout());
100 };
101 let (sequence_id, chunk) = chunk?;
102
103 let row_count = chunk.len() as u64;
104
105 match chunk.dtype() {
106 DType::Utf8(n) => {
107 truncate_scalar_stat(chunk.statistics(), Stat::Min, |v| {
108 lower_bound(
109 BufferString::from_scalar(v)
110 .vortex_expect("utf8 scalar must be a BufferString"),
111 self.max_variable_length_statistics_size,
112 *n,
113 )
114 });
115 truncate_scalar_stat(chunk.statistics(), Stat::Max, |v| {
116 upper_bound(
117 BufferString::from_scalar(v)
118 .vortex_expect("utf8 scalar must be a BufferString"),
119 self.max_variable_length_statistics_size,
120 *n,
121 )
122 });
123 }
124 DType::Binary(n) => {
125 truncate_scalar_stat(chunk.statistics(), Stat::Min, |v| {
126 lower_bound(
127 ByteBuffer::from_scalar(v)
128 .vortex_expect("binary scalar must be a ByteBuffer"),
129 self.max_variable_length_statistics_size,
130 *n,
131 )
132 });
133 truncate_scalar_stat(chunk.statistics(), Stat::Max, |v| {
134 upper_bound(
135 ByteBuffer::from_scalar(v)
136 .vortex_expect("binary scalar must be a ByteBuffer"),
137 self.max_variable_length_statistics_size,
138 *n,
139 )
140 });
141 }
142 _ => {}
143 }
144
145 let buffers = chunk.serialize(
146 ctx.array_ctx(),
147 session,
148 &SerializeOptions {
149 offset: 0,
150 include_padding: self.include_padding,
151 },
152 )?;
153 assert!(buffers.len() >= 2);
155 let array_node =
156 flat_layout_inline_array_node().then(|| buffers[buffers.len() - 2].clone());
157 let segment_id = segment_sink.write(sequence_id, buffers).await?;
158
159 let None = stream.next().await else {
160 vortex_bail!("flat layout received stream with more than a single chunk");
161 };
162 Ok(FlatLayout::new_with_metadata(
163 row_count,
164 stream.dtype().clone(),
165 segment_id,
166 ReadContext::new(ctx.array_ctx().to_ids()),
167 array_node,
168 )
169 .into_layout())
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use std::sync::Arc;
176
177 use vortex_array::ArrayContext;
178 use vortex_array::ArrayRef;
179 use vortex_array::IntoArray;
180 use vortex_array::MaskFuture;
181 use vortex_array::VortexSessionExecute;
182 use vortex_array::array_session;
183 use vortex_array::arrays::BoolArray;
184 use vortex_array::arrays::Dict;
185 use vortex_array::arrays::DictArray;
186 use vortex_array::arrays::PrimitiveArray;
187 use vortex_array::arrays::StructArray;
188 use vortex_array::arrays::struct_::StructArrayExt;
189 use vortex_array::builders::ArrayBuilder;
190 use vortex_array::builders::VarBinViewBuilder;
191 use vortex_array::dtype::DType;
192 use vortex_array::dtype::FieldName;
193 use vortex_array::dtype::FieldNames;
194 use vortex_array::dtype::Nullability;
195 use vortex_array::expr::root;
196 use vortex_array::expr::stats::Precision;
197 use vortex_array::expr::stats::Stat;
198 use vortex_array::expr::stats::StatsProviderExt;
199 use vortex_array::validity::Validity;
200 use vortex_array::vtable::VTable;
201 use vortex_buffer::BitBufferMut;
202 use vortex_buffer::buffer;
203 use vortex_error::VortexExpect;
204 use vortex_error::VortexResult;
205 use vortex_io::runtime::single::block_on;
206 use vortex_io::session::RuntimeSessionExt;
207 use vortex_mask::AllOr;
208 use vortex_mask::Mask;
209 use vortex_utils::aliases::hash_set::HashSet;
210
211 use crate::LayoutStrategy;
212 use crate::LayoutStrategyEncodingValidator;
213 use crate::layouts::flat::writer::FlatLayoutStrategy;
214 use crate::segments::TestSegments;
215 use crate::sequence::SequenceId;
216 use crate::sequence::SequentialArrayStreamExt;
217 use crate::test::SESSION;
218 use crate::test::new_session;
219
220 #[should_panic]
223 #[test]
224 fn flat_stats() {
225 block_on(|handle| async {
226 let session = new_session().with_handle(handle);
227 let ctx = ArrayContext::empty();
228 let segments = Arc::new(TestSegments::default());
229 let (ptr, eof) = SequenceId::root().split();
230 let array = PrimitiveArray::new(buffer![1, 2, 3, 4, 5], Validity::AllValid);
231 let layout = FlatLayoutStrategy::default()
232 .write_stream(
233 ctx.into(),
234 Arc::<TestSegments>::clone(&segments),
235 array.into_array().to_array_stream().sequenced(ptr),
236 eof,
237 &session,
238 )
239 .await
240 .unwrap();
241
242 let reader = layout
243 .new_reader("".into(), segments, &SESSION, &Default::default())
244 .unwrap();
245 let expr = root().bind(reader.dtype()).unwrap();
246 let result = reader
247 .projection_evaluation(
248 &(0..layout.row_count()),
249 &expr,
250 MaskFuture::new_true(layout.row_count().try_into().unwrap()),
251 )
252 .unwrap()
253 .await
254 .unwrap();
255
256 assert_eq!(
257 result.statistics().get_as::<bool>(Stat::IsSorted),
258 Precision::Exact(true)
259 );
260 })
261 }
262
263 #[test]
264 fn truncates_variable_size_stats() {
265 block_on(|handle| async {
266 let session = new_session().with_handle(handle);
267 let ctx = ArrayContext::empty();
268 let segments = Arc::new(TestSegments::default());
269 let (ptr, eof) = SequenceId::root().split();
270 let mut builder =
271 VarBinViewBuilder::with_capacity(DType::Utf8(Nullability::NonNullable), 2);
272 builder.append_value("Long value to test that the statistics are actually truncated, it needs a bit of extra padding though");
273 builder.append_value("Another string that's meant to be smaller than the previous value, though still need extra padding");
274 let array = builder.finish();
275 let mut stats_ctx = session.create_execution_ctx();
276 array.statistics().set_iter(
277 array
278 .statistics()
279 .compute_all(&Stat::all().collect::<Vec<_>>(), &mut stats_ctx)
280 .vortex_expect("stats computation should succeed for test array")
281 .into_iter(),
282 );
283
284 let layout = FlatLayoutStrategy::default()
285 .write_stream(
286 ctx.into(),
287 Arc::<TestSegments>::clone(&segments),
288 array.into_array().to_array_stream().sequenced(ptr),
289 eof,
290 &session,
291 )
292 .await
293 .unwrap();
294
295 let reader = layout
296 .new_reader("".into(), segments, &SESSION, &Default::default())
297 .unwrap();
298 let expr = root().bind(reader.dtype()).unwrap();
299 let result = reader
300 .projection_evaluation(
301 &(0..layout.row_count()),
302 &expr,
303 MaskFuture::new_true(layout.row_count().try_into().unwrap()),
304 )
305 .unwrap()
306 .await
307 .unwrap();
308
309 assert_eq!(
310 result.statistics().get_as::<String>(Stat::Min),
311 Precision::Inexact(
313 "Another string that's meant to be smaller than the previous valu".to_string()
315 )
316 );
317 assert_eq!(
318 result.statistics().get_as::<String>(Stat::Max),
319 Precision::Inexact(
320 "Long value to test that the statistics are actually truncated, j".to_string()
321 )
322 );
323 })
324 }
325
326 #[test]
327 fn struct_array_round_trip() {
328 block_on(|handle| async {
329 let mut ctx_exec = array_session().create_execution_ctx();
330 let session = new_session().with_handle(handle);
331 let mut validity_builder = BitBufferMut::with_capacity(2);
332 validity_builder.append(true);
333 validity_builder.append(false);
334 let validity_boolean_buffer = validity_builder.freeze();
335 let validity = Validity::Array(
336 BoolArray::new(validity_boolean_buffer.clone(), Validity::NonNullable).into_array(),
337 );
338 let array = StructArray::try_new(
339 FieldNames::from([FieldName::from("a"), FieldName::from("b")]),
340 vec![
341 buffer![1_u64, 2].into_array(),
342 buffer![3_u64, 4].into_array(),
343 ],
344 2,
345 validity,
346 )
347 .unwrap();
348
349 let ctx = ArrayContext::empty();
350
351 let (layout, segments) = {
353 let segments = Arc::new(TestSegments::default());
354 let (ptr, eof) = SequenceId::root().split();
355 let layout = FlatLayoutStrategy::default()
356 .write_stream(
357 ctx.into(),
358 Arc::<TestSegments>::clone(&segments),
359 array.into_array().to_array_stream().sequenced(ptr),
360 eof,
361 &session,
362 )
363 .await
364 .unwrap();
365
366 (layout, segments)
367 };
368
369 let reader = layout
371 .new_reader("".into(), segments, &SESSION, &Default::default())
372 .unwrap();
373 let expr = root().bind(reader.dtype()).unwrap();
374 let result: ArrayRef = reader
375 .projection_evaluation(
376 &(0..layout.row_count()),
377 &expr,
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.into(),
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.into(),
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}