Skip to main content

vortex_array/arrays/struct_/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::borrow::Borrow;
5use std::iter::once;
6
7use itertools::Itertools;
8use vortex_array_macros::array_slots;
9use vortex_error::VortexExpect;
10use vortex_error::VortexResult;
11use vortex_error::vortex_bail;
12use vortex_error::vortex_err;
13
14use crate::ArrayRef;
15use crate::ArraySlots;
16use crate::IntoArray;
17use crate::array::Array;
18use crate::array::ArrayParts;
19use crate::array::EmptyArrayData;
20use crate::array::TypedArrayRef;
21use crate::array::child_to_validity;
22use crate::array::validity_to_child;
23use crate::arrays::ChunkedArray;
24use crate::arrays::Struct;
25use crate::builtins::ArrayBuiltins;
26use crate::dtype::DType;
27use crate::dtype::FieldName;
28use crate::dtype::FieldNames;
29use crate::dtype::StructFields;
30use crate::validity::Validity;
31
32// StructArray has a variable number of slots: [validity?, field_0, ..., field_N]
33/// Slot layout of a [`Struct`] array: `[validity?, fields...]`.
34#[array_slots(Struct)]
35pub struct StructSlots {
36    /// The optional row-level validity child.
37    #[slot(0)]
38    pub validity: Option<ArrayRef>,
39    /// The field arrays, one per struct field, all sharing the outer length.
40    #[slot(1..)]
41    pub fields: Vec<ArrayRef>,
42}
43
44/// A struct array that stores multiple named fields as columns, similar to a database row.
45///
46/// This mirrors the Apache Arrow Struct array encoding and provides a columnar representation
47/// of structured data where each row contains multiple named fields of potentially different types.
48///
49/// ## Data Layout
50///
51/// The struct array uses a columnar layout where:
52/// - Each field is stored as a separate child array
53/// - All fields must have the same length (number of rows)
54/// - Field names and types are defined in the struct's dtype
55/// - An optional validity mask indicates which entire rows are null
56///
57/// ## Row-level nulls
58///
59/// The StructArray contains its own top-level nulls, which are superimposed on top of the
60/// field-level validity values. This can be the case even if the fields themselves are non-nullable,
61/// accessing a particular row can yield nulls even if all children are valid at that position.
62///
63/// ```
64/// use vortex_array::arrays::{StructArray, BoolArray};
65/// use vortex_array::validity::Validity;
66/// use vortex_array::dtype::FieldNames;
67/// use vortex_array::{IntoArray, VortexSessionExecute, array_session};
68/// use vortex_buffer::buffer;
69///
70/// // Create struct with all non-null fields but struct-level nulls
71/// let struct_array = StructArray::try_new(
72///     FieldNames::from(["a", "b", "c"]),
73///     vec![
74///         buffer![1i32, 2i32].into_array(),  // non-null field a
75///         buffer![10i32, 20i32].into_array(), // non-null field b
76///         buffer![100i32, 200i32].into_array(), // non-null field c
77///     ],
78///     2,
79///     Validity::Array(BoolArray::from_iter([true, false]).into_array()), // row 1 is null
80/// ).unwrap();
81/// let mut ctx = array_session().create_execution_ctx();
82///
83/// // Row 0 is valid - returns a struct scalar with field values
84/// let row0 = struct_array.execute_scalar(0, &mut ctx).unwrap();
85/// assert!(!row0.is_null());
86///
87/// // Row 1 is null at struct level - returns null even though fields have values
88/// let row1 = struct_array.execute_scalar(1, &mut ctx).unwrap();
89/// assert!(row1.is_null());
90/// ```
91///
92/// ## Name uniqueness
93///
94/// It is valid for a StructArray to have multiple child columns that have the same name. In this
95/// case, any accessors that use column names will find the first column in sequence with the name.
96///
97/// ```
98/// use vortex_array::arrays::StructArray;
99/// use vortex_array::arrays::struct_::StructArrayExt;
100/// use vortex_array::validity::Validity;
101/// use vortex_array::dtype::FieldNames;
102/// use vortex_array::{IntoArray, VortexSessionExecute, array_session};
103/// use vortex_buffer::buffer;
104///
105/// // Create struct with duplicate "data" field names
106/// let struct_array = StructArray::try_new(
107///     FieldNames::from(["data", "data"]),
108///     vec![
109///         buffer![1i32, 2i32].into_array(),   // first "data"
110///         buffer![3i32, 4i32].into_array(),   // second "data"
111///     ],
112///     2,
113///     Validity::NonNullable,
114/// ).unwrap();
115///
116/// // field_by_name returns the FIRST "data" field
117/// let first_data = struct_array.unmasked_field_by_name("data").unwrap();
118/// let mut ctx = array_session().create_execution_ctx();
119/// assert_eq!(first_data.execute_scalar(0, &mut ctx).unwrap(), 1i32.into());
120/// ```
121///
122/// ## Field Operations
123///
124/// Struct arrays support efficient column operations:
125/// - **Projection**: Select/reorder fields without copying data
126/// - **Field access**: Get columns by name or index
127/// - **Column addition**: Add new fields to create extended structs
128/// - **Column removal**: Remove fields to create narrower structs
129///
130/// ## Validity Semantics
131///
132/// - Row-level nulls are tracked in the struct's validity child
133/// - Individual field nulls are tracked in each field's own validity
134/// - A null struct row means all fields in that row are conceptually null
135/// - Field-level nulls can exist independently of struct-level nulls
136///
137/// # Examples
138///
139/// ```
140/// use vortex_array::arrays::{StructArray, PrimitiveArray};
141/// use vortex_array::arrays::struct_::StructArrayExt;
142/// use vortex_array::validity::Validity;
143/// use vortex_array::dtype::FieldNames;
144/// use vortex_array::IntoArray;
145/// use vortex_buffer::buffer;
146///
147/// // Create arrays for each field
148/// let ids = PrimitiveArray::new(buffer![1i32, 2, 3], Validity::NonNullable);
149/// let names = PrimitiveArray::new(buffer![100u64, 200, 300], Validity::NonNullable);
150///
151/// // Create struct array with named fields
152/// let struct_array = StructArray::try_new(
153///     FieldNames::from(["id", "score"]),
154///     vec![ids.into_array(), names.into_array()],
155///     3,
156///     Validity::NonNullable,
157/// ).unwrap();
158///
159/// assert_eq!(struct_array.len(), 3);
160/// assert_eq!(struct_array.names().len(), 2);
161///
162/// // Access field by name
163/// let id_field = struct_array.unmasked_field_by_name("id").unwrap();
164/// assert_eq!(id_field.len(), 3);
165/// ```
166pub struct StructDataParts {
167    pub struct_fields: StructFields,
168    pub fields: Vec<ArrayRef>,
169    pub validity: Validity,
170}
171
172/// Allocate the slots of a [`Struct`] array holding only its validity, with room for `nfields`
173/// field slots to be pushed.
174///
175/// Callers that build their field arrays fallibly use this directly, so they don't have to stage
176/// them in a `Vec` just to hand them over as an infallible iterator.
177pub(super) fn struct_slots_with_capacity(
178    validity: &Validity,
179    length: usize,
180    nfields: usize,
181) -> ArraySlots {
182    let mut slots = ArraySlots::with_capacity(StructSlots::FIELDS_OFFSET + nfields);
183    slots.push(validity_to_child(validity, length));
184    slots
185}
186
187pub(super) fn make_struct_slots(
188    fields: impl IntoIterator<Item = ArrayRef>,
189    validity: &Validity,
190    length: usize,
191) -> ArraySlots {
192    // Take the fields by value so callers that already own them move their `ArrayRef`s straight
193    // into the `SmallVec` instead of paying a refcount bump per field.
194    let fields = fields.into_iter();
195    let mut slots = struct_slots_with_capacity(validity, length, fields.size_hint().0);
196    slots.extend(fields.map(Some));
197    slots
198}
199
200/// Struct-specific accessors.
201///
202/// Slot accessors (`validity`, `fields`, `slots_view`) live on the generated
203/// [`StructArraySlotsExt`] supertrait; this trait layers struct-specific lookups on top.
204pub trait StructArrayExt: StructArraySlotsExt {
205    fn nullability(&self) -> crate::dtype::Nullability {
206        match self.as_ref().dtype() {
207            DType::Struct(_, nullability) => *nullability,
208            _ => unreachable!("StructArrayExt requires a struct dtype"),
209        }
210    }
211
212    fn names(&self) -> &FieldNames {
213        self.as_ref().dtype().as_struct_fields().names()
214    }
215
216    fn struct_validity(&self) -> Validity {
217        child_to_validity(self.validity(), self.nullability())
218    }
219
220    /// Iterate over the field arrays in declaration order.
221    fn iter_unmasked_fields(&self) -> impl ExactSizeIterator<Item = &ArrayRef> + '_ {
222        self.fields().iter()
223    }
224
225    /// The field array at `idx`, or `None` if `idx` is out of bounds.
226    ///
227    /// Use this over [`unmasked_field`](Self::unmasked_field) when the index comes from outside
228    /// the library and an out-of-bounds value is an error to report rather than a bug to panic on.
229    fn unmasked_field_opt(&self, idx: usize) -> Option<&ArrayRef> {
230        self.fields().get(idx)
231    }
232
233    /// The field array at `idx`.
234    ///
235    /// # Panics
236    ///
237    /// If `idx` is out of bounds.
238    fn unmasked_field(&self, idx: usize) -> &ArrayRef {
239        self.unmasked_field_opt(idx)
240            .vortex_expect("StructArray field slot")
241    }
242
243    fn unmasked_field_by_name_opt(&self, name: impl AsRef<str>) -> Option<&ArrayRef> {
244        let name = name.as_ref();
245        self.struct_fields()
246            .find(name)
247            .map(|idx| self.unmasked_field(idx))
248    }
249
250    fn unmasked_field_by_name(&self, name: impl AsRef<str>) -> VortexResult<&ArrayRef> {
251        let name = name.as_ref();
252        self.unmasked_field_by_name_opt(name).ok_or_else(|| {
253            vortex_err!(
254                "Field {name} not found in struct array with names {:?}",
255                self.names()
256            )
257        })
258    }
259
260    fn struct_fields(&self) -> &StructFields {
261        self.as_ref().dtype().as_struct_fields()
262    }
263}
264impl<T: TypedArrayRef<Struct>> StructArrayExt for T {}
265
266impl Array<Struct> {
267    /// Creates a new `StructArray`.
268    pub fn new(
269        names: FieldNames,
270        fields: impl IntoIterator<Item = ArrayRef>,
271        length: usize,
272        validity: Validity,
273    ) -> Self {
274        Self::try_new(names, fields, length, validity)
275            .vortex_expect("StructArray construction failed")
276    }
277
278    /// Constructs a new `StructArray`.
279    pub fn try_new(
280        names: FieldNames,
281        fields: impl IntoIterator<Item = ArrayRef>,
282        length: usize,
283        validity: Validity,
284    ) -> VortexResult<Self> {
285        let fields = fields.into_iter();
286        let (lower, _) = fields.size_hint();
287        let mut field_dtypes = Vec::with_capacity(lower);
288        let mut slots = ArraySlots::with_capacity(StructSlots::FIELDS_OFFSET + lower);
289        slots.push(validity_to_child(&validity, length));
290        for field in fields {
291            field_dtypes.push(field.dtype().clone());
292            slots.push(Some(field));
293        }
294        let dtype = StructFields::new(names, field_dtypes);
295        Array::try_from_parts(ArrayParts::new(
296            Struct,
297            DType::Struct(dtype, validity.nullability()),
298            length,
299            EmptyArrayData,
300            slots,
301        ))
302    }
303
304    /// Creates a new `StructArray` without validation.
305    ///
306    /// # Safety
307    ///
308    /// Caller must ensure the field arrays match the supplied dtype, length, and validity.
309    pub unsafe fn new_unchecked(
310        fields: impl IntoIterator<Item = ArrayRef>,
311        dtype: StructFields,
312        length: usize,
313        validity: Validity,
314    ) -> Self {
315        let outer_dtype = DType::Struct(dtype, validity.nullability());
316        let slots = make_struct_slots(fields, &validity, length);
317        unsafe {
318            Array::from_parts_unchecked(ArrayParts::new(
319                Struct,
320                outer_dtype,
321                length,
322                EmptyArrayData,
323                slots,
324            ))
325        }
326    }
327
328    /// Constructs a new `StructArray` with an explicit dtype.
329    pub fn try_new_with_dtype(
330        fields: impl IntoIterator<Item = ArrayRef>,
331        dtype: StructFields,
332        length: usize,
333        validity: Validity,
334    ) -> VortexResult<Self> {
335        let outer_dtype = DType::Struct(dtype, validity.nullability());
336        let slots = make_struct_slots(fields, &validity, length);
337        Array::try_from_parts(ArrayParts::new(
338            Struct,
339            outer_dtype,
340            length,
341            EmptyArrayData,
342            slots,
343        ))
344    }
345
346    /// Construct a `StructArray` from named fields.
347    pub fn from_fields<N: AsRef<str>>(items: &[(N, ArrayRef)]) -> VortexResult<Self> {
348        Self::try_from_iter(items.iter().map(|(a, b)| (a, b.clone())))
349    }
350
351    /// Create a `StructArray` from an iterator of (name, array) pairs with validity.
352    pub fn try_from_iter_with_validity<
353        N: AsRef<str>,
354        A: IntoArray,
355        T: IntoIterator<Item = (N, A)>,
356    >(
357        iter: T,
358        validity: Validity,
359    ) -> VortexResult<Self> {
360        let (names, fields): (Vec<FieldName>, Vec<ArrayRef>) = iter
361            .into_iter()
362            .map(|(name, fields)| (FieldName::from(name.as_ref()), fields.into_array()))
363            .unzip();
364        let len = fields
365            .first()
366            .map(|f| f.len())
367            .ok_or_else(|| vortex_err!("StructArray cannot be constructed from an empty slice of arrays because the length is unspecified"))?;
368
369        Self::try_new(FieldNames::from_iter(names), fields, len, validity)
370    }
371
372    /// Create a `StructArray` from an iterator of (name, array) pairs.
373    pub fn try_from_iter<N: AsRef<str>, A: IntoArray, T: IntoIterator<Item = (N, A)>>(
374        iter: T,
375    ) -> VortexResult<Self> {
376        let (names, fields): (Vec<FieldName>, Vec<ArrayRef>) = iter
377            .into_iter()
378            .map(|(name, field)| (FieldName::from(name.as_ref()), field.into_array()))
379            .unzip();
380        let len = fields
381            .first()
382            .map(ArrayRef::len)
383            .ok_or_else(|| vortex_err!("StructArray cannot be constructed from an empty slice of arrays because the length is unspecified"))?;
384
385        Self::try_new(
386            FieldNames::from_iter(names),
387            fields,
388            len,
389            Validity::NonNullable,
390        )
391    }
392
393    // TODO(aduffy): Add equivalent function to support field masks for nested column access.
394    /// Return a new StructArray with the given projection applied.
395    ///
396    /// Projection does not copy data arrays. Projection is defined by an ordinal array slice
397    /// which specifies the new ordering of columns in the struct. The projection can be used to
398    /// perform column re-ordering, deletion, or duplication at a logical level, without any data
399    /// copying.
400    pub fn project(&self, projection: &[FieldName]) -> VortexResult<Self> {
401        let mut children = Vec::with_capacity(projection.len());
402        let mut names = Vec::with_capacity(projection.len());
403
404        for f_name in projection {
405            let idx = self
406                .struct_fields()
407                .find(f_name.as_ref())
408                .ok_or_else(|| vortex_err!("Unknown field {f_name}"))?;
409
410            names.push(self.names()[idx].clone());
411            children.push(self.unmasked_field(idx).clone());
412        }
413
414        Self::try_new(
415            FieldNames::from(names.as_slice()),
416            children,
417            self.len(),
418            self.validity()?,
419        )
420    }
421
422    /// Create a fieldless `StructArray` with the given length.
423    pub fn new_fieldless_with_len(len: usize) -> Self {
424        let dtype = DType::Struct(
425            StructFields::new(FieldNames::default(), Vec::new()),
426            crate::dtype::Nullability::NonNullable,
427        );
428        let slots = make_struct_slots([], &Validity::NonNullable, len);
429        unsafe {
430            Array::from_parts_unchecked(ArrayParts::new(Struct, dtype, len, EmptyArrayData, slots))
431        }
432    }
433
434    // TODO(ngates): remove this... it doesn't help to consume self.
435    pub fn into_data_parts(self) -> StructDataParts {
436        let fields = self.fields().to_vec();
437        let validity = self.validity().vortex_expect("StructArray validity");
438        StructDataParts {
439            struct_fields: self.struct_fields().clone(),
440            fields,
441            validity,
442        }
443    }
444
445    pub fn remove_column(&self, name: impl Into<FieldName>) -> Option<(Self, ArrayRef)> {
446        let name = name.into();
447        let struct_dtype = self.struct_fields();
448        let len = self.len();
449
450        let position = struct_dtype.find(name.as_ref())?;
451
452        let slot_position = StructSlots::FIELDS_OFFSET + position;
453        let field = self.unmasked_field(position).clone();
454        // `Filter` has a zero lower bound, so build the slots with an exact capacity instead of
455        // letting `collect` grow them.
456        let slots = self.slots();
457        let mut new_slots = ArraySlots::with_capacity(slots.len() - 1);
458        new_slots.extend(slots[..slot_position].iter().cloned());
459        new_slots.extend(slots[slot_position + 1..].iter().cloned());
460
461        let new_dtype = struct_dtype.without_field(position).ok()?;
462        let new_array = unsafe {
463            Array::from_parts_unchecked(ArrayParts::new(
464                Struct,
465                DType::Struct(new_dtype, self.dtype().nullability()),
466                len,
467                EmptyArrayData,
468                new_slots,
469            ))
470        };
471        Some((new_array, field))
472    }
473
474    pub fn with_column(&self, name: impl Into<FieldName>, array: ArrayRef) -> VortexResult<Self> {
475        let name = name.into();
476        let struct_dtype = self.struct_fields();
477
478        let names = struct_dtype.names().iter().cloned().chain(once(name));
479        let types = struct_dtype.fields().chain(once(array.dtype().clone()));
480        let new_fields = StructFields::new(names.collect(), types.collect());
481
482        let children = self.iter_unmasked_fields().cloned().chain(once(array));
483
484        Self::try_new_with_dtype(children, new_fields, self.len(), self.validity()?)
485    }
486
487    pub fn remove_column_owned(&self, name: impl Into<FieldName>) -> Option<(Self, ArrayRef)> {
488        self.remove_column(name)
489    }
490
491    pub fn try_concat<T>(chunks: impl IntoIterator<Item = T>) -> VortexResult<Self>
492    where
493        T: Borrow<Array<Struct>>,
494    {
495        let mut it = chunks.into_iter();
496        let Some(first) = it.next() else {
497            vortex_bail!("cannot concat empty iterator of arrays");
498        };
499        let first_dtype = first.borrow().dtype().clone();
500        let struct_fields = first_dtype.as_struct_fields().clone();
501        let names = struct_fields.names();
502
503        let it = [first].into_iter().chain(it);
504        let (field_arrays_per_chunk, validities) = it
505            .map(|chunk| {
506                let chunk = chunk.borrow();
507                if &first_dtype != chunk.dtype() {
508                    vortex_bail!(
509                        "cannot concatenate struct arrays with differing dtypes: {}, {}",
510                        first_dtype,
511                        chunk.dtype(),
512                    );
513                }
514
515                let fields = names
516                    .iter()
517                    .map(|name| {
518                        chunk
519                            .unmasked_field_by_name(name)
520                            .vortex_expect("field exists because it is in dtype")
521                            .clone()
522                    })
523                    .collect::<Vec<_>>();
524                let validity = chunk.validity()?;
525
526                Ok((fields, (validity, chunk.len())))
527            })
528            .process_results(|iter| iter.unzip::<_, _, Vec<_>, Vec<_>>())?;
529
530        let field_arrays = struct_fields
531            .fields()
532            .enumerate()
533            .map(|(i, dtype)| {
534                // SAFETY: We establish above that every array has the same type.
535                let chunks = field_arrays_per_chunk.iter().map(|x| x[i].clone());
536                unsafe { ChunkedArray::new_unchecked(chunks, dtype) }.into_array()
537            })
538            .collect::<Vec<_>>();
539        let len = validities.iter().map(|(_v, len)| len).sum();
540        let validity = Validity::concat(validities).vortex_expect("verified non-empty above");
541
542        // SAFETY:
543        //
544        // 1. The field arrays, by construction, have the type specified in fields.
545        //
546        // 2. Each Array<Struct> has a valid len, therefore the sum of those lens should be valid
547        // for the concatenation of each field.
548        //
549        // 3. Each Array<Struct> has a valid validity, so the concatenation of those validities has
550        // the correct length and dtype harmony.
551        Ok(unsafe { Array::<Struct>::new_unchecked(field_arrays, struct_fields, len, validity) })
552    }
553
554    /// Push the struct's top-level validity into each field, so a row null at the struct level
555    /// becomes null in every field.
556    ///
557    /// If `remove_struct_validity` is set the result is non-nullable; otherwise it keeps its
558    /// top-level validity.
559    pub fn push_validity_into_children(&self, remove_struct_validity: bool) -> VortexResult<Self> {
560        let struct_validity = self.struct_validity();
561
562        let new_validity = if remove_struct_validity {
563            Validity::NonNullable
564        } else {
565            struct_validity.clone()
566        };
567
568        // Nothing to push down. The fields are unchanged, so reuse the existing `StructFields`
569        // rather than re-deriving it field by field.
570        if struct_validity.definitely_no_nulls() {
571            return Self::try_new_with_dtype(
572                self.iter_unmasked_fields().cloned(),
573                self.struct_fields().clone(),
574                self.len(),
575                new_validity,
576            );
577        }
578
579        // Null each field where the struct row is null.
580        let mask = struct_validity.to_array(self.len());
581        let fields = self
582            .iter_unmasked_fields()
583            .map(|field| field.clone().mask(mask.clone()))
584            .collect::<VortexResult<Vec<_>>>()?;
585
586        Self::try_new(self.names().clone(), fields, self.len(), new_validity)
587    }
588}