Skip to main content

vortex_layout/plan/plans/
pack.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::borrow::Cow;
5
6use vortex_array::EmptyMetadata;
7use vortex_array::dtype::DType;
8use vortex_array::dtype::FieldName;
9use vortex_array::dtype::FieldNames;
10use vortex_array::dtype::Nullability;
11use vortex_array::dtype::StructFields;
12use vortex_array::expr::BoundExpression;
13use vortex_array::expr::ExactBoundExpr;
14use vortex_array::expr::descendent_bound_annotations;
15use vortex_array::expr::make_bound_free_field_annotator;
16use vortex_array::expr::transform::partition_bound;
17use vortex_array::expr::traversal::NodeExt;
18use vortex_array::expr::traversal::Transformed;
19use vortex_array::expr::traversal::TraversalOrder;
20use vortex_array::scalar_fn::ScalarFnVTableExt;
21use vortex_array::scalar_fn::fns::get_item::GetItem;
22use vortex_array::scalar_fn::fns::pack::Pack as PackFn;
23use vortex_array::scalar_fn::fns::pack::PackOptions;
24use vortex_array::scalar_fn::fns::select::Select;
25use vortex_error::VortexExpect;
26use vortex_error::VortexResult;
27use vortex_error::vortex_bail;
28use vortex_error::vortex_ensure;
29use vortex_error::vortex_err;
30use vortex_session::registry::CachedId;
31
32use crate::plan::Eval;
33use crate::plan::EvalPlan;
34use crate::plan::Plan;
35use crate::plan::PlanChildren;
36use crate::plan::PlanId;
37use crate::plan::PlanParts;
38use crate::plan::PlanRef;
39use crate::plan::PlanVTable;
40use crate::plan::optimizer::PlanParentReduceRule;
41
42/// Assembles a struct from one child per field, plus an optional trailing validity child.
43#[derive(Clone, Debug)]
44pub struct Pack;
45
46/// Operator-specific struct assembly data.
47#[derive(Clone, Debug)]
48pub struct PackData;
49
50/// A plan that assembles a struct from its children.
51pub type PackPlan = Plan<Pack>;
52
53impl PackPlan {
54    /// Creates a struct assembly from potentially unresolved children without validation.
55    ///
56    /// # Safety
57    ///
58    /// `children` must contain one child per field, followed by a non-nullable boolean validity
59    /// child exactly when `nullability` is nullable. Every child must have `row_count` rows, and
60    /// every field child must have its corresponding field dtype.
61    pub(crate) unsafe fn from_children_unchecked(
62        fields: StructFields,
63        nullability: Nullability,
64        row_count: u64,
65        children: PlanChildren,
66    ) -> Self {
67        PlanParts {
68            vtable: Pack,
69            dtype: DType::Struct(fields, nullability),
70            row_count,
71            children,
72            data: PackData,
73        }
74        .into_typed()
75    }
76
77    /// Creates a struct assembly from `fields` and one child per field.
78    ///
79    /// Each field child must have the corresponding field dtype and `row_count` rows. `validity`
80    /// is required exactly when `nullability` is [`Nullability::Nullable`] and must produce
81    /// non-nullable booleans with `row_count` rows.
82    pub fn try_new(
83        fields: StructFields,
84        nullability: Nullability,
85        row_count: u64,
86        field_plans: Vec<PlanRef>,
87        validity: Option<PlanRef>,
88    ) -> VortexResult<Self> {
89        if field_plans.len() != fields.nfields() {
90            vortex_bail!(
91                "Pack expects {} field children but got {}",
92                fields.nfields(),
93                field_plans.len()
94            );
95        }
96        if validity.is_some() != (nullability == Nullability::Nullable) {
97            vortex_bail!("Pack validity child must be present exactly when the struct is nullable");
98        }
99
100        for (index, (field_dtype, field_plan)) in
101            fields.fields().zip(field_plans.iter()).enumerate()
102        {
103            validate_field_child(index, &field_dtype, row_count, field_plan)?;
104        }
105        if let Some(validity) = validity.as_ref() {
106            validate_validity_child(row_count, validity)?;
107        }
108
109        // SAFETY: The child count, presence, dtypes, and row counts were validated above.
110        Ok(unsafe { Self::new_unchecked(fields, nullability, row_count, field_plans, validity) })
111    }
112
113    /// Creates a struct assembly without validating its children.
114    ///
115    /// # Safety
116    ///
117    /// The caller must ensure that:
118    ///
119    /// - `field_plans` contains exactly one child per field, in field order;
120    /// - every field child has the corresponding field dtype and `row_count` rows; and
121    /// - `validity` is present exactly when the struct is nullable and, when present, produces
122    ///   non-nullable booleans with `row_count` rows.
123    pub unsafe fn new_unchecked(
124        fields: StructFields,
125        nullability: Nullability,
126        row_count: u64,
127        field_plans: Vec<PlanRef>,
128        validity: Option<PlanRef>,
129    ) -> Self {
130        let mut children = field_plans;
131        children.extend(validity);
132        // SAFETY: The caller guarantees the same child invariants required by this constructor.
133        unsafe { Self::from_children_unchecked(fields, nullability, row_count, children.into()) }
134    }
135
136    /// Returns the struct fields assembled by this plan.
137    pub fn fields(&self) -> &StructFields {
138        self.dtype()
139            .as_struct_fields_opt()
140            .vortex_expect("Pack dtype must be a struct")
141    }
142
143    /// Returns the number of struct fields, excluding any validity child.
144    pub fn nfields(&self) -> usize {
145        self.fields().nfields()
146    }
147
148    /// Returns the plan producing struct validity, if the struct is nullable.
149    pub fn validity(&self) -> VortexResult<Option<PlanRef>> {
150        self.child(self.nfields())
151    }
152}
153
154impl PlanVTable for Pack {
155    type PlanData = PackData;
156    type Metadata = EmptyMetadata;
157
158    fn id(&self) -> PlanId {
159        static ID: CachedId = CachedId::new("vortex.plan.pack");
160        *ID
161    }
162
163    fn metadata(_plan: &Plan<Self>) -> Option<Self::Metadata> {
164        // The struct fields are recoverable from the plan dtype.
165        Some(EmptyMetadata)
166    }
167
168    fn with_children(
169        plan: &Plan<Self>,
170        children: &PlanChildren,
171        _data: &mut Self::PlanData,
172    ) -> VortexResult<()> {
173        let expected_children = plan.nfields() + usize::from(plan.dtype().is_nullable());
174        if children.len() != expected_children {
175            vortex_bail!(
176                "Pack expects {expected_children} children but got {}",
177                children.len()
178            );
179        }
180
181        for (index, field_dtype) in plan.fields().fields().enumerate() {
182            let child = children
183                .get(index)?
184                .ok_or_else(|| vortex_err!("Pack field child {index} is absent"))?;
185            validate_field_child(index, &field_dtype, plan.row_count(), &child)?;
186        }
187        if plan.dtype().is_nullable() {
188            let validity = children
189                .get(plan.nfields())?
190                .ok_or_else(|| vortex_err!("Pack validity child is absent"))?;
191            validate_validity_child(plan.row_count(), &validity)?;
192        }
193        Ok(())
194    }
195
196    fn child_name(plan: &Plan<Self>, index: usize) -> Cow<'_, str> {
197        assert!(
198            index < plan.children().len(),
199            "Pack child index out of bounds: {index} of {}",
200            plan.children().len()
201        );
202        if let Some(name) = plan.fields().field_name(index) {
203            return Cow::Borrowed(name.as_ref());
204        }
205        Cow::Borrowed("validity")
206    }
207}
208
209fn validate_field_child(
210    index: usize,
211    expected_dtype: &DType,
212    expected_row_count: u64,
213    child: &PlanRef,
214) -> VortexResult<()> {
215    if child.dtype() != expected_dtype {
216        vortex_bail!(
217            "Pack field child {index} has dtype {} but the field has dtype {expected_dtype}",
218            child.dtype()
219        );
220    }
221    if child.row_count() != expected_row_count {
222        vortex_bail!(
223            "Pack field child {index} has {} rows but the plan has {expected_row_count}",
224            child.row_count()
225        );
226    }
227    Ok(())
228}
229
230fn validate_validity_child(expected_row_count: u64, child: &PlanRef) -> VortexResult<()> {
231    let expected_dtype = DType::Bool(Nullability::NonNullable);
232    if child.dtype() != &expected_dtype {
233        vortex_bail!(
234            "Pack validity child has dtype {} but must have dtype {expected_dtype}",
235            child.dtype()
236        );
237    }
238    if child.row_count() != expected_row_count {
239        vortex_bail!(
240            "Pack validity child has {} rows but the plan has {expected_row_count}",
241            child.row_count()
242        );
243    }
244    Ok(())
245}
246
247impl PackPlan {
248    /// Rebuilds this plan with only `fields`, which must be a subset of the current fields.
249    ///
250    /// Pruning is only sound for a non-nullable struct: dropping a field of a nullable struct
251    /// would drop the validity child that the remaining fields depend on.
252    pub(crate) fn with_pruned_fields(
253        &self,
254        fields: Vec<(FieldName, PlanRef)>,
255    ) -> VortexResult<Self> {
256        vortex_ensure!(
257            !self.dtype().is_nullable(),
258            "Cannot prune fields from a nullable Pack"
259        );
260        let struct_fields = StructFields::from_iter(
261            fields
262                .iter()
263                .map(|(name, plan)| (name.clone(), plan.dtype().clone())),
264        );
265        let field_plans = fields.into_iter().map(|(_, plan)| plan).collect::<Vec<_>>();
266        PackPlan::try_new(
267            struct_fields,
268            Nullability::NonNullable,
269            self.row_count(),
270            field_plans,
271            None,
272        )
273    }
274}
275
276/// Pushes an expression into the referenced fields of a [`Pack`], pruning the rest.
277#[derive(Debug)]
278pub(crate) struct ExpressionPackRule;
279
280impl PlanParentReduceRule<Pack> for ExpressionPackRule {
281    type Parent = Eval;
282
283    fn reduce_parent(
284        &self,
285        child: &Plan<Pack>,
286        parent: &Plan<Eval>,
287        _child_idx: usize,
288    ) -> VortexResult<Option<PlanRef>> {
289        if child.dtype().is_nullable() {
290            return Ok(None);
291        }
292
293        let expression = parent.expression();
294        let fields = child.fields();
295        let referenced_fields =
296            descendent_bound_annotations(expression, make_bound_free_field_annotator(fields))
297                .get(&ExactBoundExpr(expression.clone()))
298                .vortex_expect("Bound expression missing free-field annotations")
299                .clone();
300        let expanded_root = expanded_struct_root(child.dtype(), fields)?;
301        let expanded = expand_struct_root(expression.clone(), &expanded_root, fields)?;
302        let partitioned =
303            partition_bound(expanded.clone(), make_bound_free_field_annotator(fields))?;
304
305        if partitioned.partition_names.is_empty() {
306            let selected_indices = fields
307                .names()
308                .iter()
309                .enumerate()
310                .filter_map(|(index, name)| referenced_fields.contains(name).then_some(index))
311                .collect::<Vec<_>>();
312            if selected_indices.len() == fields.nfields() {
313                return Ok(None);
314            }
315
316            let pruned_fields = selected_indices
317                .into_iter()
318                .map(|field_index| {
319                    Ok((
320                        field_name(fields, field_index)?,
321                        field_plan(child, field_index)?,
322                    ))
323                })
324                .collect::<VortexResult<Vec<_>>>()?;
325            let rewritten = child.with_pruned_fields(pruned_fields)?.into_plan();
326            return Ok(Some(
327                EvalPlan::try_new(expression.clone(), rewritten)?.into_plan(),
328            ));
329        }
330
331        if partitioned.partition_names.len() == 1 {
332            let name = partitioned
333                .partition_names
334                .get(0)
335                .ok_or_else(|| vortex_err!("Struct expression partition has no field"))?;
336            let index = fields.find(name).ok_or_else(|| {
337                vortex_err!("Struct expression references unknown field '{name}'")
338            })?;
339            let field = field_plan(child, index)?;
340            let lowered = step_into_struct_field(expanded, name, field.dtype().clone())?;
341            return Ok(Some(EvalPlan::try_new(lowered, field)?.into_plan()));
342        }
343
344        let residual = partitioned.root;
345        let mut collapsed = Vec::with_capacity(partitioned.partitions.len());
346        let mut field_expressions = vec![None; fields.nfields()];
347        for index in 0..partitioned.partitions.len() {
348            let name = &partitioned.partition_names[index];
349            let partition = &partitioned.partitions[index];
350            let field_index = fields.find(name).ok_or_else(|| {
351                vortex_err!("Struct expression references unknown field '{name}'")
352            })?;
353            let field = field_plan(child, field_index)?;
354            let lowered = if let Some(pack) = partition
355                .as_scalar()
356                .and_then(|scalar_fn| scalar_fn.as_opt::<PackFn>())
357                && partition.children().len() == 1
358            {
359                let value_name = pack
360                    .names
361                    .get(0)
362                    .ok_or_else(|| vortex_err!("Struct expression partition pack is empty"))?;
363                collapsed.push((name.clone(), value_name.clone()));
364                partition.children()[0].clone()
365            } else {
366                partition.clone()
367            };
368            let lowered = step_into_struct_field(lowered, name, field.dtype().clone())?;
369            field_expressions[field_index] = Some(lowered);
370        }
371
372        let mut fields_changed = partitioned.partition_names.len() != fields.nfields();
373        let mut pruned_fields = Vec::with_capacity(partitioned.partition_names.len());
374        for (field_index, expression) in field_expressions.into_iter().enumerate() {
375            let Some(expression) = expression else {
376                continue;
377            };
378            let field = field_plan(child, field_index)?;
379            let field = if is_identity_expression(&expression, field.dtype())? {
380                field
381            } else {
382                fields_changed = true;
383                EvalPlan::try_new(expression, field)?.into_plan()
384            };
385            pruned_fields.push((field_name(fields, field_index)?, field));
386        }
387        let rewritten = if fields_changed {
388            child.with_pruned_fields(pruned_fields)?.into_plan()
389        } else {
390            child.to_plan()
391        };
392        let residual = rewrite_partition_root(residual, rewritten.dtype().clone(), &collapsed)?;
393
394        if !fields_changed && residual == *expression {
395            return Ok(None);
396        }
397
398        Ok(Some(EvalPlan::try_new(residual, rewritten)?.into_plan()))
399    }
400}
401
402/// Rebinds a partitioned residual expression after collapsing single-value partitions.
403///
404/// # Arguments
405///
406/// * `expression` - The residual recombination expression returned by `partition_bound`.
407/// * `root_dtype` - The dtype produced by the rewritten plan and used to rebind every root.
408/// * `collapsed` - `(partition_name, value_name)` pairs whose one-field `Pack` was removed;
409///   each `$.partition_name.value_name` access is rewritten to `$.partition_name`.
410pub(super) fn rewrite_partition_root(
411    expression: BoundExpression,
412    root_dtype: DType,
413    collapsed: &[(FieldName, FieldName)],
414) -> VortexResult<BoundExpression> {
415    Ok(expression
416        .transform_down(|node| {
417            if let Some(value_name) = node.as_opt::<GetItem>() {
418                let partition_access = &node.children()[0];
419                if let Some(partition_name) = partition_access.as_opt::<GetItem>()
420                    && partition_access.children()[0].is_root()
421                    && collapsed.iter().any(|(partition, value)| {
422                        partition == partition_name && value == value_name
423                    })
424                {
425                    return Ok(Transformed {
426                        value: BoundExpression::try_new(
427                            GetItem.bind(partition_name.clone()),
428                            [BoundExpression::new_root(root_dtype.clone())],
429                        )?,
430                        changed: true,
431                        order: TraversalOrder::Skip,
432                    });
433                }
434            }
435
436            if node.is_root() {
437                Ok(Transformed {
438                    value: BoundExpression::new_root(root_dtype.clone()),
439                    changed: true,
440                    order: TraversalOrder::Skip,
441                })
442            } else {
443                Ok(Transformed::no(node))
444            }
445        })?
446        .into_inner())
447}
448
449fn field_name(fields: &StructFields, index: usize) -> VortexResult<FieldName> {
450    Ok(fields
451        .field_name(index)
452        .ok_or_else(|| vortex_err!("Struct field {index} has no name"))?
453        .clone())
454}
455
456fn field_plan(plan: &Plan<Pack>, index: usize) -> VortexResult<PlanRef> {
457    plan.child(index)?
458        .ok_or_else(|| vortex_err!("Struct field {index} has no plan"))
459}
460
461fn expanded_struct_root(
462    root_dtype: &DType,
463    fields: &StructFields,
464) -> VortexResult<BoundExpression> {
465    let root = BoundExpression::new_root(root_dtype.clone());
466    let children = fields
467        .names()
468        .iter()
469        .map(|name| BoundExpression::try_new(GetItem.bind(name.clone()), [root.clone()]))
470        .collect::<VortexResult<Vec<_>>>()?;
471    bound_pack(fields.names().clone(), children)
472}
473
474fn is_identity_expression(expression: &BoundExpression, input_dtype: &DType) -> VortexResult<bool> {
475    if expression.is_root() {
476        return Ok(expression.dtype() == input_dtype);
477    }
478    if input_dtype.is_nullable() {
479        return Ok(false);
480    }
481    let Some(fields) = input_dtype.as_struct_fields_opt() else {
482        return Ok(false);
483    };
484    Ok(expression == &expanded_struct_root(input_dtype, fields)?)
485}
486
487fn expand_struct_root(
488    expression: BoundExpression,
489    expanded_root: &BoundExpression,
490    fields: &StructFields,
491) -> VortexResult<BoundExpression> {
492    Ok(expression
493        .transform_down(|node| {
494            if node.is_root() {
495                return Ok(Transformed {
496                    value: expanded_root.clone(),
497                    changed: true,
498                    order: TraversalOrder::Skip,
499                });
500            }
501
502            let Some(scalar_fn) = node.as_scalar() else {
503                return Ok(Transformed::no(node));
504            };
505            if !node
506                .children()
507                .first()
508                .is_some_and(BoundExpression::is_root)
509            {
510                return Ok(Transformed::no(node));
511            }
512
513            if let Some(field_name) = scalar_fn.as_opt::<GetItem>() {
514                let index = fields.find(field_name).ok_or_else(|| {
515                    vortex_err!("Field {field_name} not found while expanding struct root")
516                })?;
517                return Ok(Transformed {
518                    value: expanded_root.children()[index].clone(),
519                    changed: true,
520                    order: TraversalOrder::Skip,
521                });
522            }
523
524            if let Some(selection) = scalar_fn.as_opt::<Select>() {
525                let names = selection.normalize_to_included_fields(fields.names())?;
526                let children = names
527                    .iter()
528                    .map(|name| {
529                        let index = fields
530                            .find(name)
531                            .vortex_expect("normalized selection fields must exist in the root");
532                        expanded_root.children()[index].clone()
533                    })
534                    .collect();
535                return Ok(Transformed {
536                    value: bound_pack(names, children)?,
537                    changed: true,
538                    order: TraversalOrder::Skip,
539                });
540            }
541
542            Ok(Transformed::no(node))
543        })?
544        .into_inner())
545}
546
547fn step_into_struct_field(
548    expression: BoundExpression,
549    field_name: &FieldName,
550    field_dtype: DType,
551) -> VortexResult<BoundExpression> {
552    Ok(expression
553        .transform_down(|node| {
554            let is_field_access = node
555                .as_scalar()
556                .and_then(|scalar_fn| scalar_fn.as_opt::<GetItem>())
557                .is_some_and(|name| name == field_name)
558                && node.children()[0].is_root();
559
560            if is_field_access {
561                Ok(Transformed {
562                    value: BoundExpression::new_root(field_dtype.clone()),
563                    changed: true,
564                    order: TraversalOrder::Skip,
565                })
566            } else {
567                Ok(Transformed::no(node))
568            }
569        })?
570        .into_inner())
571}
572
573fn bound_pack(names: FieldNames, children: Vec<BoundExpression>) -> VortexResult<BoundExpression> {
574    BoundExpression::try_new(
575        PackFn.bind(PackOptions {
576            names,
577            nullability: Nullability::NonNullable,
578        }),
579        children,
580    )
581}