vortex_layout/layouts/struct_/
writer.rs1use std::sync::Arc;
15
16use async_trait::async_trait;
17use futures::StreamExt;
18use futures::TryStreamExt;
19use futures::future::try_join;
20use futures::future::try_join_all;
21use futures::pin_mut;
22use itertools::Itertools;
23use vortex_array::ArrayRef;
24use vortex_array::IntoArray;
25use vortex_array::VortexSessionExecute;
26use vortex_array::arrays::StructArray;
27use vortex_array::arrays::struct_::StructArrayExt;
28use vortex_array::dtype::DType;
29use vortex_array::dtype::FieldName;
30use vortex_array::dtype::Nullability;
31use vortex_error::VortexError;
32use vortex_error::VortexResult;
33use vortex_error::vortex_bail;
34use vortex_io::kanal_ext::KanalExt;
35use vortex_io::session::RuntimeSessionExt;
36use vortex_session::VortexSession;
37use vortex_utils::aliases::DefaultHashBuilder;
38use vortex_utils::aliases::hash_map::HashMap;
39use vortex_utils::aliases::hash_set::HashSet;
40
41use crate::LayoutRef;
42use crate::LayoutStrategy;
43use crate::LayoutWriterContext;
44use crate::layouts::struct_::StructLayout;
45use crate::segments::SegmentSinkRef;
46use crate::sequence::SendableSequentialStream;
47use crate::sequence::SequenceId;
48use crate::sequence::SequencePointer;
49use crate::sequence::SequentialStreamAdapter;
50use crate::sequence::SequentialStreamExt;
51
52#[derive(Clone)]
63pub struct StructStrategy {
64 field_writers: HashMap<FieldName, Arc<dyn LayoutStrategy>>,
67 default: Arc<dyn LayoutStrategy>,
69 validity: Arc<dyn LayoutStrategy>,
71}
72
73impl StructStrategy {
74 pub fn new(validity: Arc<dyn LayoutStrategy>, default: Arc<dyn LayoutStrategy>) -> Self {
77 Self {
78 field_writers: HashMap::default(),
79 default,
80 validity,
81 }
82 }
83
84 pub fn with_field_writer(
86 mut self,
87 name: impl Into<FieldName>,
88 writer: Arc<dyn LayoutStrategy>,
89 ) -> Self {
90 self.field_writers.insert(name.into(), writer);
91 self
92 }
93
94 pub fn with_field_writers(
96 mut self,
97 writers: impl IntoIterator<Item = (FieldName, Arc<dyn LayoutStrategy>)>,
98 ) -> Self {
99 self.field_writers.extend(writers);
100 self
101 }
102}
103
104#[async_trait]
105impl LayoutStrategy for StructStrategy {
106 async fn write_stream(
107 &self,
108 ctx: LayoutWriterContext,
109 segment_sink: SegmentSinkRef,
110 stream: SendableSequentialStream,
111 mut eof: SequencePointer,
112 session: &VortexSession,
113 ) -> VortexResult<LayoutRef> {
114 let dtype = stream.dtype().clone();
115
116 let Some(struct_dtype) = dtype.as_struct_fields_opt() else {
117 vortex_bail!("StructStrategy can only write struct-typed streams, got {dtype}");
118 };
119
120 if HashSet::<_, DefaultHashBuilder>::from_iter(struct_dtype.names().iter()).len()
122 != struct_dtype.names().len()
123 {
124 vortex_bail!("StructLayout must have unique field names");
125 }
126 let is_nullable = dtype.is_nullable();
127
128 if struct_dtype.nfields() == 0 && !is_nullable {
131 let row_count = stream
132 .try_fold(
133 0u64,
134 |acc, (_, arr)| async move { Ok(acc + arr.len() as u64) },
135 )
136 .await?;
137 return Ok(StructLayout::new(row_count, dtype, vec![]).into_layout());
138 }
139
140 let columns_session = session.clone();
142 let columns_vec_stream = stream.map(move |chunk| {
143 let (sequence_id, chunk) = chunk?;
144 let mut sequence_pointer = sequence_id.descend();
145 let mut ctx = columns_session.create_execution_ctx();
146 let struct_chunk = chunk.clone().execute::<StructArray>(&mut ctx)?;
147 let mut columns: Vec<(SequenceId, ArrayRef)> = Vec::new();
148 if is_nullable {
149 columns.push((
150 sequence_pointer.advance(),
151 chunk
152 .validity()?
153 .execute_mask(chunk.len(), &mut ctx)?
154 .into_array(),
155 ));
156 }
157
158 columns.extend(
159 struct_chunk
160 .iter_unmasked_fields()
161 .map(|field| (sequence_pointer.advance(), field.clone())),
162 );
163
164 Ok(columns)
165 });
166
167 let mut stream_count = struct_dtype.nfields();
168 if is_nullable {
169 stream_count += 1;
170 }
171
172 let (column_streams_tx, column_streams_rx): (Vec<_>, Vec<_>) =
173 (0..stream_count).map(|_| kanal::bounded_async(1)).unzip();
174
175 let handle = session.handle();
178 let fanout_fut = async move {
179 pin_mut!(columns_vec_stream);
180 while let Some(result) = columns_vec_stream.next().await {
181 match result {
182 Ok(columns) => {
183 for (tx, column) in column_streams_tx.iter().zip_eq(columns) {
184 if tx.send(Ok(column)).await.is_err() {
185 vortex_bail!(
186 "struct column writer finished before all chunks were sent"
187 );
188 }
189 }
190 }
191 Err(e) => {
192 let e: Arc<VortexError> = Arc::new(e);
193 for tx in column_streams_tx.iter() {
194 let _ = tx.send(Err(VortexError::from(Arc::clone(&e)))).await;
195 }
196 return Err(VortexError::from(e));
197 }
198 }
199 }
200 Ok(())
201 };
202
203 let column_dtypes: Vec<DType> = if is_nullable {
205 std::iter::once(DType::Bool(Nullability::NonNullable))
206 .chain(struct_dtype.fields())
207 .collect()
208 } else {
209 struct_dtype.fields().collect()
210 };
211
212 let column_names: Vec<FieldName> = if is_nullable {
213 std::iter::once(FieldName::from("__validity"))
214 .chain(struct_dtype.names().iter().cloned())
215 .collect()
216 } else {
217 struct_dtype.names().iter().cloned().collect()
218 };
219
220 let layout_futures: Vec<_> = column_dtypes
221 .into_iter()
222 .zip_eq(column_streams_rx)
223 .zip_eq(column_names)
224 .enumerate()
225 .map(move |(index, ((dtype, recv), name))| {
226 let column_stream =
227 SequentialStreamAdapter::new(dtype, recv.into_stream().boxed()).sendable();
228 let child_eof = eof.split_off();
229 let session = session.clone();
230 let is_validity = index == 0 && is_nullable;
231 let ctx = if is_nullable && !is_validity {
235 ctx.clone().with_nullable_ancestor()
236 } else {
237 ctx.clone()
238 };
239 let segment_sink = Arc::clone(&segment_sink);
240 handle.spawn_nested(move |_| {
241 let writer = if is_validity {
244 Arc::clone(&self.validity)
245 } else {
246 self.field_writers
247 .get(&name)
248 .cloned()
249 .unwrap_or_else(|| Arc::clone(&self.default))
250 };
251
252 async move {
253 writer
254 .write_stream(ctx, segment_sink, column_stream, child_eof, &session)
255 .await
256 }
257 })
258 })
259 .collect();
260
261 let (_success, column_layouts) = try_join(fanout_fut, try_join_all(layout_futures)).await?;
262 let row_count = column_layouts.first().map(|l| l.row_count()).unwrap_or(0);
265 Ok(StructLayout::new(row_count, dtype, column_layouts).into_layout())
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use std::num::NonZeroUsize;
272
273 use vortex_array::ArrayContext;
274 use vortex_array::arrays::BoolArray;
275 use vortex_array::validity::Validity;
276 use vortex_buffer::buffer;
277 use vortex_error::VortexExpect;
278 use vortex_io::runtime::single::block_on;
279 use vortex_io::session::RuntimeSessionExt;
280
281 use super::*;
282 use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
283 use crate::layouts::flat::writer::FlatLayoutStrategy;
284 use crate::layouts::table::TableStrategy;
285 use crate::layouts::zoned::Zoned;
286 use crate::layouts::zoned::writer::ZonedLayoutOptions;
287 use crate::layouts::zoned::writer::ZonedStrategy;
288 use crate::segments::TestSegments;
289 use crate::sequence::SequentialArrayStreamExt;
290 use crate::test::new_session;
291
292 fn field_has_zone_map(validity: Validity) -> VortexResult<bool> {
294 let strategy = StructStrategy::new(
295 Arc::new(FlatLayoutStrategy::default()),
296 Arc::new(ZonedStrategy::new(
297 ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()),
298 FlatLayoutStrategy::default(),
299 ZonedLayoutOptions {
300 block_size: NonZeroUsize::new(3).vortex_expect("non zero"),
301 ..Default::default()
302 },
303 )),
304 );
305 let is_nullable = validity.nullability().is_nullable();
306 let (ptr, eof) = SequenceId::root().split();
307 let stream = StructArray::try_from_iter_with_validity(
308 [("a", buffer![1, 2, 3].into_array())],
309 validity,
310 )?
311 .into_array()
312 .to_array_stream()
313 .sequenced(ptr);
314
315 let layout = block_on(|handle| async move {
316 let session = new_session().with_handle(handle);
317 strategy
318 .write_stream(
319 LayoutWriterContext::new(ArrayContext::empty()),
320 Arc::new(TestSegments::default()),
321 stream,
322 eof,
323 &session,
324 )
325 .await
326 })?;
327
328 Ok(layout.children()?[usize::from(is_nullable)].is::<Zoned>())
330 }
331
332 fn write_table(array: StructArray) -> VortexResult<LayoutRef> {
334 let strategy = TableStrategy::new(
335 Arc::new(FlatLayoutStrategy::default()),
336 Arc::new(ZonedStrategy::new(
337 ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()),
338 FlatLayoutStrategy::default(),
339 ZonedLayoutOptions {
340 block_size: NonZeroUsize::new(3).vortex_expect("non zero"),
341 ..Default::default()
342 },
343 )),
344 );
345 let (ptr, eof) = SequenceId::root().split();
346 let stream = array.into_array().to_array_stream().sequenced(ptr);
347 block_on(|handle| async move {
348 let session = new_session().with_handle(handle);
349 strategy
350 .write_stream(
351 LayoutWriterContext::new(ArrayContext::empty()),
352 Arc::new(TestSegments::default()),
353 stream,
354 eof,
355 &session,
356 )
357 .await
358 })
359 }
360
361 #[test]
362 fn non_nullable_struct_fields_get_a_zone_map() -> VortexResult<()> {
363 assert!(field_has_zone_map(Validity::NonNullable)?);
364 Ok(())
365 }
366
367 #[test]
370 fn nullable_struct_fields_get_no_zone_map() -> VortexResult<()> {
371 assert!(!field_has_zone_map(Validity::Array(
372 BoolArray::from_iter([false, true, true]).into_array(),
373 ))?);
374 Ok(())
375 }
376
377 #[test]
380 fn nullable_ancestor_propagates_through_nested_structs() -> VortexResult<()> {
381 let some_nulls = || Validity::Array(BoolArray::from_iter([false, true, true]).into_array());
382
383 let outer_nullable = StructArray::try_from_iter_with_validity(
385 [(
386 "inner",
387 StructArray::try_from_iter([("a", buffer![1, 2, 3].into_array())])?.into_array(),
388 )],
389 some_nulls(),
390 )?;
391 let layout = write_table(outer_nullable)?;
392 let inner = &layout.children()?[1];
393 assert!(!inner.children()?[0].is::<Zoned>());
394
395 let inner_nullable = StructArray::try_from_iter([
398 ("sibling", buffer![1, 2, 3].into_array()),
399 (
400 "inner",
401 StructArray::try_from_iter_with_validity(
402 [("a", buffer![1, 2, 3].into_array())],
403 some_nulls(),
404 )?
405 .into_array(),
406 ),
407 ])?;
408 let layout = write_table(inner_nullable)?;
409 let children = layout.children()?;
410 assert!(children[0].is::<Zoned>());
411 assert!(!children[1].children()?[1].is::<Zoned>());
412 Ok(())
413 }
414
415 fn nest(leaf: ArrayRef, depth: usize) -> VortexResult<ArrayRef> {
417 (0..depth).try_fold(leaf, |inner, level| {
418 Ok(StructArray::try_from_iter([(format!("l{level}"), inner)])?.into_array())
419 })
420 }
421
422 fn descend(layout: &LayoutRef, depth: usize) -> VortexResult<LayoutRef> {
424 (0..depth).try_fold(Arc::clone(layout), |layout, _| {
425 Ok(Arc::clone(&layout.children()?[0]))
426 })
427 }
428
429 #[test]
432 fn nullable_ancestor_propagates_through_deep_nesting() -> VortexResult<()> {
433 const DEPTH: usize = 6;
434 let some_nulls = || Validity::Array(BoolArray::from_iter([false, true, true]).into_array());
435
436 let root = StructArray::try_from_iter_with_validity(
438 [("top", nest(buffer![1, 2, 3].into_array(), DEPTH)?)],
439 some_nulls(),
440 )?;
441 let layout = write_table(root)?;
442 let leaf = descend(&layout.children()?[1], DEPTH)?;
444 assert!(!leaf.is::<Zoned>());
445
446 let middle = StructArray::try_from_iter_with_validity(
449 [("below", nest(buffer![1, 2, 3].into_array(), DEPTH)?)],
450 some_nulls(),
451 )?;
452 let root = nest(
453 StructArray::try_from_iter([
454 ("above", buffer![1, 2, 3].into_array()),
455 ("middle", middle.into_array()),
456 ])?
457 .into_array(),
458 DEPTH,
459 )?
460 .execute::<StructArray>(&mut new_session().create_execution_ctx())?;
461 let layout = write_table(root)?;
462 let holder = descend(&layout, DEPTH)?;
463 let holder_children = holder.children()?;
464 assert!(holder_children[0].is::<Zoned>());
465 let leaf = descend(&holder_children[1].children()?[1], DEPTH)?;
467 assert!(!leaf.is::<Zoned>());
468 Ok(())
469 }
470}