Skip to main content

vortex_layout/plan/physical/plans/
list_pack.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::borrow::Cow;
5use std::sync::Arc;
6
7use vortex_array::EmptyMetadata;
8use vortex_array::dtype::DType;
9use vortex_array::dtype::Nullability;
10use vortex_error::VortexResult;
11use vortex_error::vortex_bail;
12use vortex_error::vortex_err;
13use vortex_session::registry::CachedId;
14
15use crate::plan::physical::Plan;
16use crate::plan::physical::PlanChildren;
17use crate::plan::physical::PlanId;
18use crate::plan::physical::PlanParts;
19use crate::plan::physical::PlanRef;
20use crate::plan::physical::PlanVTable;
21
22const ELEMENTS: usize = 0;
23const OFFSETS: usize = 1;
24const VALIDITY: usize = 2;
25
26/// Assembles a list from elements and offsets, plus an optional trailing validity child.
27#[derive(Clone, Debug)]
28pub struct ListPack;
29
30/// Operator-specific list assembly data.
31#[derive(Clone, Debug)]
32pub struct ListPackData;
33
34/// A plan that assembles a list from its children.
35pub type ListPackPlan = Plan<ListPack>;
36
37impl ListPackPlan {
38    /// Creates a list assembly from potentially unresolved children without validation.
39    ///
40    /// # Safety
41    ///
42    /// `dtype` must be a list whose element dtype matches the elements child. The offsets child
43    /// must be a non-nullable integer with `row_count + 1` rows. A non-nullable boolean validity
44    /// child with `row_count` rows must be present exactly when `dtype` is nullable.
45    pub(crate) unsafe fn from_children_unchecked(
46        dtype: DType,
47        row_count: u64,
48        children: PlanChildren,
49    ) -> Self {
50        PlanParts {
51            vtable: ListPack,
52            dtype,
53            row_count,
54            children,
55            data: ListPackData,
56        }
57        .into_typed()
58    }
59
60    /// Creates a list assembly from `elements` and `offsets`.
61    ///
62    /// `validity` is required exactly when `nullability` is [`Nullability::Nullable`]. The row
63    /// domain is one fewer than the number of offsets.
64    pub fn try_new(
65        nullability: Nullability,
66        row_count: u64,
67        elements: PlanRef,
68        offsets: PlanRef,
69        validity: Option<PlanRef>,
70    ) -> VortexResult<Self> {
71        let dtype = DType::List(Arc::new(elements.dtype().clone()), nullability);
72        let mut children = vec![elements, offsets];
73        children.extend(validity);
74        let children = PlanChildren::from(children);
75        validate_children(&dtype, row_count, &children)?;
76
77        // SAFETY: All child shape invariants were validated above.
78        Ok(unsafe { Self::from_children_unchecked(dtype, row_count, children) })
79    }
80
81    /// Returns the plan producing list elements.
82    pub fn elements(&self) -> VortexResult<PlanRef> {
83        self.child_required(ELEMENTS)
84    }
85
86    /// Returns the plan producing list offsets.
87    pub fn offsets(&self) -> VortexResult<PlanRef> {
88        self.child_required(OFFSETS)
89    }
90
91    /// Returns the plan producing list validity, if the list is nullable.
92    pub fn validity(&self) -> VortexResult<Option<PlanRef>> {
93        self.child(VALIDITY)
94    }
95}
96
97impl PlanVTable for ListPack {
98    type PlanData = ListPackData;
99    type Metadata = EmptyMetadata;
100
101    fn id(&self) -> PlanId {
102        static ID: CachedId = CachedId::new("vortex.plan.list_pack");
103        *ID
104    }
105
106    fn metadata(_plan: &Plan<Self>) -> Option<Self::Metadata> {
107        // Nullability is recoverable from the plan dtype.
108        Some(EmptyMetadata)
109    }
110
111    fn with_children(
112        plan: &Plan<Self>,
113        children: &PlanChildren,
114        _data: &mut Self::PlanData,
115    ) -> VortexResult<()> {
116        validate_children(plan.dtype(), plan.row_count(), children)
117    }
118
119    fn child_name(_plan: &Plan<Self>, index: usize) -> Cow<'_, str> {
120        match index {
121            ELEMENTS => Cow::Borrowed("elements"),
122            OFFSETS => Cow::Borrowed("offsets"),
123            VALIDITY => Cow::Borrowed("validity"),
124            _ => Cow::Owned(format!("child[{index}]")),
125        }
126    }
127}
128
129fn validate_children(dtype: &DType, row_count: u64, children: &PlanChildren) -> VortexResult<()> {
130    let elements_dtype = dtype
131        .as_list_element_opt()
132        .ok_or_else(|| vortex_err!("ListPack output dtype must be a list, got {dtype}"))?;
133    let expected_children = 2 + usize::from(dtype.is_nullable());
134    if children.len() != expected_children {
135        vortex_bail!(
136            "ListPack expects {expected_children} children but got {}",
137            children.len()
138        );
139    }
140
141    let elements = children
142        .get(ELEMENTS)?
143        .ok_or_else(|| vortex_err!("ListPack elements child is absent"))?;
144    if elements.dtype() != elements_dtype.as_ref() {
145        vortex_bail!(
146            "ListPack elements child has dtype {} but the list element dtype is {}",
147            elements.dtype(),
148            elements_dtype
149        );
150    }
151
152    let offsets = children
153        .get(OFFSETS)?
154        .ok_or_else(|| vortex_err!("ListPack offsets child is absent"))?;
155    if !offsets.dtype().is_int() || offsets.dtype().is_nullable() {
156        vortex_bail!(
157            "ListPack offsets child must have a non-nullable integer dtype, got {}",
158            offsets.dtype()
159        );
160    }
161    let offsets_row_count = row_count
162        .checked_add(1)
163        .ok_or_else(|| vortex_err!("ListPack offsets row count overflow"))?;
164    if offsets.row_count() != offsets_row_count {
165        vortex_bail!(
166            "ListPack offsets child has {} rows but must have {offsets_row_count}",
167            offsets.row_count()
168        );
169    }
170
171    if dtype.is_nullable() {
172        let validity = children
173            .get(VALIDITY)?
174            .ok_or_else(|| vortex_err!("ListPack validity child is absent"))?;
175        let validity_dtype = DType::Bool(Nullability::NonNullable);
176        if validity.dtype() != &validity_dtype {
177            vortex_bail!(
178                "ListPack validity child has dtype {} but must have dtype {validity_dtype}",
179                validity.dtype()
180            );
181        }
182        if validity.row_count() != row_count {
183            vortex_bail!(
184                "ListPack validity child has {} rows but the plan has {row_count}",
185                validity.row_count()
186            );
187        }
188    }
189    Ok(())
190}