Skip to main content

vortex_layout/plan/physical/plans/
pack.rs

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