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(
296            ArrayParts::new(
297                Struct,
298                DType::Struct(dtype, validity.nullability()),
299                length,
300                EmptyArrayData,
301            )
302            .with_slots(slots),
303        )
304    }
305
306    /// Creates a new `StructArray` without validation.
307    ///
308    /// # Safety
309    ///
310    /// Caller must ensure the field arrays match the supplied dtype, length, and validity.
311    pub unsafe fn new_unchecked(
312        fields: impl IntoIterator<Item = ArrayRef>,
313        dtype: StructFields,
314        length: usize,
315        validity: Validity,
316    ) -> Self {
317        let outer_dtype = DType::Struct(dtype, validity.nullability());
318        let slots = make_struct_slots(fields, &validity, length);
319        unsafe {
320            Array::from_parts_unchecked(
321                ArrayParts::new(Struct, outer_dtype, length, EmptyArrayData).with_slots(slots),
322            )
323        }
324    }
325
326    /// Constructs a new `StructArray` with an explicit dtype.
327    pub fn try_new_with_dtype(
328        fields: impl IntoIterator<Item = ArrayRef>,
329        dtype: StructFields,
330        length: usize,
331        validity: Validity,
332    ) -> VortexResult<Self> {
333        let outer_dtype = DType::Struct(dtype, validity.nullability());
334        let slots = make_struct_slots(fields, &validity, length);
335        Array::try_from_parts(
336            ArrayParts::new(Struct, outer_dtype, length, EmptyArrayData).with_slots(slots),
337        )
338    }
339
340    /// Construct a `StructArray` from named fields.
341    pub fn from_fields<N: AsRef<str>>(items: &[(N, ArrayRef)]) -> VortexResult<Self> {
342        Self::try_from_iter(items.iter().map(|(a, b)| (a, b.clone())))
343    }
344
345    /// Create a `StructArray` from an iterator of (name, array) pairs with validity.
346    pub fn try_from_iter_with_validity<
347        N: AsRef<str>,
348        A: IntoArray,
349        T: IntoIterator<Item = (N, A)>,
350    >(
351        iter: T,
352        validity: Validity,
353    ) -> VortexResult<Self> {
354        let (names, fields): (Vec<FieldName>, Vec<ArrayRef>) = iter
355            .into_iter()
356            .map(|(name, fields)| (FieldName::from(name.as_ref()), fields.into_array()))
357            .unzip();
358        let len = fields
359            .first()
360            .map(|f| f.len())
361            .ok_or_else(|| vortex_err!("StructArray cannot be constructed from an empty slice of arrays because the length is unspecified"))?;
362
363        Self::try_new(FieldNames::from_iter(names), fields, len, validity)
364    }
365
366    /// Create a `StructArray` from an iterator of (name, array) pairs.
367    pub fn try_from_iter<N: AsRef<str>, A: IntoArray, T: IntoIterator<Item = (N, A)>>(
368        iter: T,
369    ) -> VortexResult<Self> {
370        let (names, fields): (Vec<FieldName>, Vec<ArrayRef>) = iter
371            .into_iter()
372            .map(|(name, field)| (FieldName::from(name.as_ref()), field.into_array()))
373            .unzip();
374        let len = fields
375            .first()
376            .map(ArrayRef::len)
377            .ok_or_else(|| vortex_err!("StructArray cannot be constructed from an empty slice of arrays because the length is unspecified"))?;
378
379        Self::try_new(
380            FieldNames::from_iter(names),
381            fields,
382            len,
383            Validity::NonNullable,
384        )
385    }
386
387    // TODO(aduffy): Add equivalent function to support field masks for nested column access.
388    /// Return a new StructArray with the given projection applied.
389    ///
390    /// Projection does not copy data arrays. Projection is defined by an ordinal array slice
391    /// which specifies the new ordering of columns in the struct. The projection can be used to
392    /// perform column re-ordering, deletion, or duplication at a logical level, without any data
393    /// copying.
394    pub fn project(&self, projection: &[FieldName]) -> VortexResult<Self> {
395        let mut children = Vec::with_capacity(projection.len());
396        let mut names = Vec::with_capacity(projection.len());
397
398        for f_name in projection {
399            let idx = self
400                .struct_fields()
401                .find(f_name.as_ref())
402                .ok_or_else(|| vortex_err!("Unknown field {f_name}"))?;
403
404            names.push(self.names()[idx].clone());
405            children.push(self.unmasked_field(idx).clone());
406        }
407
408        Self::try_new(
409            FieldNames::from(names.as_slice()),
410            children,
411            self.len(),
412            self.validity()?,
413        )
414    }
415
416    /// Create a fieldless `StructArray` with the given length.
417    pub fn new_fieldless_with_len(len: usize) -> Self {
418        let dtype = DType::Struct(
419            StructFields::new(FieldNames::default(), Vec::new()),
420            crate::dtype::Nullability::NonNullable,
421        );
422        let slots = make_struct_slots([], &Validity::NonNullable, len);
423        unsafe {
424            Array::from_parts_unchecked(
425                ArrayParts::new(Struct, dtype, len, EmptyArrayData).with_slots(slots),
426            )
427        }
428    }
429
430    // TODO(ngates): remove this... it doesn't help to consume self.
431    pub fn into_data_parts(self) -> StructDataParts {
432        let fields = self.fields().to_vec();
433        let validity = self.validity().vortex_expect("StructArray validity");
434        StructDataParts {
435            struct_fields: self.struct_fields().clone(),
436            fields,
437            validity,
438        }
439    }
440
441    pub fn remove_column(&self, name: impl Into<FieldName>) -> Option<(Self, ArrayRef)> {
442        let name = name.into();
443        let struct_dtype = self.struct_fields();
444        let len = self.len();
445
446        let position = struct_dtype.find(name.as_ref())?;
447
448        let slot_position = StructSlots::FIELDS_OFFSET + position;
449        let field = self.unmasked_field(position).clone();
450        // `Filter` has a zero lower bound, so build the slots with an exact capacity instead of
451        // letting `collect` grow them.
452        let slots = self.slots();
453        let mut new_slots = ArraySlots::with_capacity(slots.len() - 1);
454        new_slots.extend(slots[..slot_position].iter().cloned());
455        new_slots.extend(slots[slot_position + 1..].iter().cloned());
456
457        let new_dtype = struct_dtype.without_field(position).ok()?;
458        let new_array = unsafe {
459            Array::from_parts_unchecked(
460                ArrayParts::new(
461                    Struct,
462                    DType::Struct(new_dtype, self.dtype().nullability()),
463                    len,
464                    EmptyArrayData,
465                )
466                .with_slots(new_slots),
467            )
468        };
469        Some((new_array, field))
470    }
471
472    pub fn with_column(&self, name: impl Into<FieldName>, array: ArrayRef) -> VortexResult<Self> {
473        let name = name.into();
474        let struct_dtype = self.struct_fields();
475
476        let names = struct_dtype.names().iter().cloned().chain(once(name));
477        let types = struct_dtype.fields().chain(once(array.dtype().clone()));
478        let new_fields = StructFields::new(names.collect(), types.collect());
479
480        let children = self.iter_unmasked_fields().cloned().chain(once(array));
481
482        Self::try_new_with_dtype(children, new_fields, self.len(), self.validity()?)
483    }
484
485    pub fn remove_column_owned(&self, name: impl Into<FieldName>) -> Option<(Self, ArrayRef)> {
486        self.remove_column(name)
487    }
488
489    pub fn try_concat<T>(chunks: impl IntoIterator<Item = T>) -> VortexResult<Self>
490    where
491        T: Borrow<Array<Struct>>,
492    {
493        let mut it = chunks.into_iter();
494        let Some(first) = it.next() else {
495            vortex_bail!("cannot concat empty iterator of arrays");
496        };
497        let first_dtype = first.borrow().dtype().clone();
498        let struct_fields = first_dtype.as_struct_fields().clone();
499        let names = struct_fields.names();
500
501        let it = [first].into_iter().chain(it);
502        let (field_arrays_per_chunk, validities) = it
503            .map(|chunk| {
504                let chunk = chunk.borrow();
505                if &first_dtype != chunk.dtype() {
506                    vortex_bail!(
507                        "cannot concatenate struct arrays with differing dtypes: {}, {}",
508                        first_dtype,
509                        chunk.dtype(),
510                    );
511                }
512
513                let fields = names
514                    .iter()
515                    .map(|name| {
516                        chunk
517                            .unmasked_field_by_name(name)
518                            .vortex_expect("field exists because it is in dtype")
519                            .clone()
520                    })
521                    .collect::<Vec<_>>();
522                let validity = chunk.validity()?;
523
524                Ok((fields, (validity, chunk.len())))
525            })
526            .process_results(|iter| iter.unzip::<_, _, Vec<_>, Vec<_>>())?;
527
528        let field_arrays = struct_fields
529            .fields()
530            .enumerate()
531            .map(|(i, dtype)| {
532                // SAFETY: We establish above that every array has the same type.
533                let chunks = field_arrays_per_chunk.iter().map(|x| x[i].clone());
534                unsafe { ChunkedArray::new_unchecked(chunks, dtype) }.into_array()
535            })
536            .collect::<Vec<_>>();
537        let len = validities.iter().map(|(_v, len)| len).sum();
538        let validity = Validity::concat(validities).vortex_expect("verified non-empty above");
539
540        // SAFETY:
541        //
542        // 1. The field arrays, by construction, have the type specified in fields.
543        //
544        // 2. Each Array<Struct> has a valid len, therefore the sum of those lens should be valid
545        // for the concatenation of each field.
546        //
547        // 3. Each Array<Struct> has a valid validity, so the concatenation of those validities has
548        // the correct length and dtype harmony.
549        Ok(unsafe { Array::<Struct>::new_unchecked(field_arrays, struct_fields, len, validity) })
550    }
551
552    /// Push the struct's top-level validity into each field, so a row null at the struct level
553    /// becomes null in every field.
554    ///
555    /// If `remove_struct_validity` is set the result is non-nullable; otherwise it keeps its
556    /// top-level validity.
557    pub fn push_validity_into_children(&self, remove_struct_validity: bool) -> VortexResult<Self> {
558        let struct_validity = self.struct_validity();
559
560        let new_validity = if remove_struct_validity {
561            Validity::NonNullable
562        } else {
563            struct_validity.clone()
564        };
565
566        // Nothing to push down. The fields are unchanged, so reuse the existing `StructFields`
567        // rather than re-deriving it field by field.
568        if struct_validity.definitely_no_nulls() {
569            return Self::try_new_with_dtype(
570                self.iter_unmasked_fields().cloned(),
571                self.struct_fields().clone(),
572                self.len(),
573                new_validity,
574            );
575        }
576
577        // Null each field where the struct row is null.
578        let mask = struct_validity.to_array(self.len());
579        let fields = self
580            .iter_unmasked_fields()
581            .map(|field| field.clone().mask(mask.clone()))
582            .collect::<VortexResult<Vec<_>>>()?;
583
584        Self::try_new(self.names().clone(), fields, self.len(), new_validity)
585    }
586}