Skip to main content

vortex_array/expr/transform/
bound_partition.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::fmt::Formatter;
6use std::hash::Hash;
7
8use itertools::Itertools;
9use vortex_error::VortexExpect;
10use vortex_error::VortexResult;
11use vortex_error::vortex_ensure_eq;
12use vortex_utils::aliases::hash_map::HashMap;
13
14use crate::dtype::DType;
15use crate::dtype::FieldName;
16use crate::dtype::FieldNames;
17use crate::dtype::Nullability;
18use crate::dtype::StructFields;
19use crate::expr::BoundExpression;
20use crate::expr::ExactBoundExpr;
21use crate::expr::analysis::Annotation;
22use crate::expr::analysis::AnnotationFn;
23use crate::expr::analysis::BoundAnnotations;
24use crate::expr::analysis::descendent_bound_annotations;
25use crate::expr::bound::get_item;
26use crate::expr::bound::pack;
27use crate::expr::traversal::NodeExt;
28use crate::expr::traversal::NodeRewriter;
29use crate::expr::traversal::Transformed;
30use crate::expr::traversal::TraversalOrder;
31
32/// Partition an expression into sub-expressions that are uniquely associated with an annotation.
33/// A root expression is also returned that can be used to recombine the results of the partitions
34/// into the result of the original expression.
35pub fn partition_bound<A: AnnotationFn<BoundExpression>>(
36    expr: BoundExpression,
37    annotate_fn: A,
38) -> VortexResult<BoundPartitionedExpr<A::Annotation>>
39where
40    A::Annotation: Display,
41    FieldName: From<A::Annotation>,
42{
43    // Annotate each expression with the annotations that any of its descendent expressions have.
44    let annotations = descendent_bound_annotations(&expr, annotate_fn);
45    partition_bound_annotations(expr, annotations)
46}
47
48/// Partition an already-annotated bound expression tree.
49///
50/// Prefer [`partition_bound`] when annotations can be derived by an [`AnnotationFn`].
51pub fn partition_bound_annotations<A>(
52    expr: BoundExpression,
53    annotations: BoundAnnotations<A>,
54) -> VortexResult<BoundPartitionedExpr<A>>
55where
56    A: Display + Clone + Eq + Hash,
57    FieldName: From<A>,
58{
59    let mut collector = PartitionCollector::<A>::new(&annotations);
60    expr.clone().rewrite(&mut collector)?;
61
62    let mut partitions = Vec::with_capacity(collector.sub_expressions.len());
63    let mut partition_annotations = Vec::with_capacity(collector.sub_expressions.len());
64
65    for (annotation, exprs) in collector.sub_expressions {
66        // We pack all sub-expressions for the same annotation into a single expression.
67        let names: FieldNames = exprs
68            .iter()
69            .enumerate()
70            .map(|(idx, _)| PartitionCollector::field_name(&annotation, idx))
71            .collect();
72        let expr = pack(names.into_iter().zip(exprs), Nullability::NonNullable);
73
74        partitions.push(expr);
75        partition_annotations.push(annotation);
76    }
77
78    let partition_names = partition_annotations
79        .iter()
80        .map(|id| FieldName::from(id.clone()))
81        .collect::<FieldNames>();
82    let root_scope = partition_root_dtype(&partition_names, &partitions);
83    let mut rewriter = PartitionRootRewriter::new(&annotations, root_scope);
84    let root = expr.rewrite(&mut rewriter)?.value;
85
86    Ok(BoundPartitionedExpr {
87        root,
88        partitions: partitions.into_boxed_slice(),
89        partition_names,
90        partition_annotations: partition_annotations.into_boxed_slice(),
91    })
92}
93
94/// The result of partitioning a bound expression.
95///
96/// The root and partitions remain bound so callers can cache and reuse their shared tree identity
97/// without an unbind/rebind round trip.
98#[derive(Debug)]
99pub struct BoundPartitionedExpr<A> {
100    /// The root expression used to re-assemble the results.
101    pub root: BoundExpression,
102    /// The partition expressions themselves.
103    pub partitions: Box<[BoundExpression]>,
104    /// The field name of each partition as referenced in the root expression.
105    pub partition_names: FieldNames,
106    /// The annotation associated with each partition.
107    pub partition_annotations: Box<[A]>,
108}
109
110impl<A: Display> Display for BoundPartitionedExpr<A> {
111    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
112        write!(
113            f,
114            "root: {} {{{}}}",
115            self.root,
116            self.partition_names
117                .iter()
118                .zip(self.partitions.iter())
119                .map(|(name, partition)| format!("{name}: {partition}"))
120                .join(", ")
121        )
122    }
123}
124
125impl<A: Annotation> BoundPartitionedExpr<A>
126where
127    FieldName: From<A>,
128{
129    /// Return the partition for a given field, if it exists.
130    // FIXME(ngates): this should return an iterator since an annotation may have multiple partitions.
131    pub fn find_partition(&self, id: &A) -> Option<&BoundExpression> {
132        let id = FieldName::from(id.clone());
133        self.partition_names
134            .iter()
135            .position(|field| field == id)
136            .map(|idx| &self.partitions[idx])
137    }
138
139    /// Replace the partition expressions and update every root dtype in the recombination tree.
140    pub fn replace_partitions(&mut self, partitions: Box<[BoundExpression]>) -> VortexResult<()> {
141        vortex_ensure_eq!(partitions.len(), self.partition_names.len());
142
143        let root_dtype = partition_root_dtype(&self.partition_names, &partitions);
144        let root = replace_root_dtype(self.root.clone(), root_dtype)?;
145        self.partitions = partitions;
146        self.root = root;
147        Ok(())
148    }
149}
150
151#[derive(Debug)]
152struct PartitionCollector<'a, A: Annotation> {
153    annotations: &'a BoundAnnotations<A>,
154    sub_expressions: HashMap<A, Vec<BoundExpression>>,
155}
156
157impl<'a, A: Annotation + Display> PartitionCollector<'a, A> {
158    fn new(annotations: &'a BoundAnnotations<A>) -> Self {
159        Self {
160            sub_expressions: HashMap::new(),
161            annotations,
162        }
163    }
164
165    /// Each annotation may be associated with multiple sub-expressions, so we need to
166    /// a unique name for each sub-expression.
167    fn field_name(annotation: &A, idx: usize) -> FieldName {
168        format!("{annotation}_{idx}").into()
169    }
170}
171
172impl<A: Annotation + Display> NodeRewriter for PartitionCollector<'_, A>
173where
174    FieldName: From<A>,
175{
176    type NodeTy = BoundExpression;
177
178    fn visit_down(&mut self, node: Self::NodeTy) -> VortexResult<Transformed<Self::NodeTy>> {
179        match self.annotations.get(&ExactBoundExpr(node.clone())) {
180            // If this expression only accesses a single field, then we can skip the children
181            Some(annotations) if annotations.len() == 1 => {
182                let annotation = annotations
183                    .iter()
184                    .next()
185                    .vortex_expect("expected one field");
186                let sub_exprs = self.sub_expressions.entry(annotation.clone()).or_default();
187                sub_exprs.push(node.clone());
188                Ok(Transformed {
189                    value: node,
190                    changed: false,
191                    order: TraversalOrder::Skip,
192                })
193            }
194
195            // Otherwise, continue traversing.
196            _ => Ok(Transformed::no(node)),
197        }
198    }
199
200    fn visit_up(&mut self, node: Self::NodeTy) -> VortexResult<Transformed<Self::NodeTy>> {
201        Ok(Transformed::no(node))
202    }
203}
204
205struct PartitionRootRewriter<'a, A: Annotation> {
206    annotations: &'a BoundAnnotations<A>,
207    partition_offsets: HashMap<A, usize>,
208    root_dtype: DType,
209}
210
211impl<'a, A: Annotation> PartitionRootRewriter<'a, A> {
212    fn new(annotations: &'a BoundAnnotations<A>, root_dtype: DType) -> Self {
213        Self {
214            annotations,
215            partition_offsets: HashMap::new(),
216            root_dtype,
217        }
218    }
219}
220
221impl<A: Annotation + Display> NodeRewriter for PartitionRootRewriter<'_, A>
222where
223    FieldName: From<A>,
224{
225    type NodeTy = BoundExpression;
226
227    fn visit_down(&mut self, node: Self::NodeTy) -> VortexResult<Transformed<Self::NodeTy>> {
228        let Some(annotations) = self.annotations.get(&ExactBoundExpr(node.clone())) else {
229            return Ok(Transformed::no(node));
230        };
231        if annotations.len() != 1 {
232            return Ok(Transformed::no(node));
233        }
234
235        let annotation = annotations
236            .iter()
237            .next()
238            .vortex_expect("expected one annotation");
239        let offset = self
240            .partition_offsets
241            .entry(annotation.clone())
242            .or_default();
243        let field_name = PartitionCollector::field_name(annotation, *offset);
244        *offset += 1;
245
246        let partition = get_item(
247            FieldName::from(annotation.clone()),
248            BoundExpression::new_root(self.root_dtype.clone()),
249        );
250        let value = get_item(field_name, partition);
251
252        Ok(Transformed {
253            value,
254            changed: true,
255            order: TraversalOrder::Skip,
256        })
257    }
258}
259
260fn partition_root_dtype(names: &FieldNames, partitions: &[BoundExpression]) -> DType {
261    DType::Struct(
262        StructFields::new(
263            names.clone(),
264            partitions
265                .iter()
266                .map(|partition| partition.dtype().clone())
267                .collect(),
268        ),
269        Nullability::NonNullable,
270    )
271}
272
273fn replace_root_dtype(expr: BoundExpression, root_dtype: DType) -> VortexResult<BoundExpression> {
274    Ok(expr
275        .transform_down(|node| {
276            if node.is_root() {
277                Ok(Transformed {
278                    value: BoundExpression::new_root(root_dtype.clone()),
279                    changed: true,
280                    order: TraversalOrder::Skip,
281                })
282            } else {
283                Ok(Transformed::no(node))
284            }
285        })?
286        .into_inner())
287}
288
289#[cfg(test)]
290mod tests {
291    use rstest::fixture;
292    use rstest::rstest;
293
294    use super::*;
295    use crate::dtype::DType;
296    use crate::dtype::Nullability::NonNullable;
297    use crate::dtype::Nullability::Nullable;
298    use crate::dtype::PType::I32;
299    use crate::dtype::StructFields;
300    use crate::expr::analysis::make_bound_free_field_annotator;
301    use crate::expr::checked_add;
302    use crate::expr::col;
303    use crate::expr::get_item;
304    use crate::expr::lit;
305    use crate::expr::merge;
306    use crate::expr::pack;
307    use crate::expr::root;
308
309    #[fixture]
310    fn dtype() -> DType {
311        DType::Struct(
312            StructFields::from_iter([
313                (
314                    "a",
315                    DType::Struct(
316                        StructFields::from_iter([("x", I32.into()), ("y", DType::from(I32))]),
317                        NonNullable,
318                    ),
319                ),
320                ("b", I32.into()),
321                ("c", I32.into()),
322            ]),
323            NonNullable,
324        )
325    }
326
327    fn partition_by_field(
328        expr: BoundExpression,
329        dtype: &DType,
330    ) -> VortexResult<BoundPartitionedExpr<FieldName>> {
331        let fields = dtype.as_struct_fields_opt().unwrap();
332        partition_bound(expr, make_bound_free_field_annotator(fields))
333    }
334
335    #[rstest]
336    fn test_expr_top_level_ref(dtype: DType) {
337        let fields = dtype.as_struct_fields_opt().unwrap();
338
339        let expr = root();
340        let partitioned = partition_by_field(expr.bind(&dtype).unwrap(), &dtype).unwrap();
341
342        // An un-expanded root expression is annotated by all fields, but since it is a single node
343        assert_eq!(partitioned.partitions.len(), 0);
344        assert_eq!(partitioned.root, root().bind(&dtype).unwrap());
345
346        // Instead, callers must expand the root expression themselves.
347        let expr = pack(
348            fields
349                .names()
350                .iter()
351                .map(|name| (name.clone(), col(name.clone()))),
352            NonNullable,
353        );
354        let partitioned = partition_by_field(expr.bind(&dtype).unwrap(), &dtype).unwrap();
355
356        assert_eq!(partitioned.partitions.len(), fields.names().len());
357        for name in fields.names().iter() {
358            let expected = pack([(format!("{name}_0"), col(name.clone()))], NonNullable)
359                .bind(&dtype)
360                .unwrap();
361            assert_eq!(partitioned.find_partition(name), Some(&expected));
362        }
363    }
364
365    #[rstest]
366    fn test_expr_top_level_ref_get_item_and_split(dtype: DType) {
367        let expr = get_item("y", get_item("a", root()));
368
369        let partitioned = partition_by_field(expr.bind(&dtype).unwrap(), &dtype).unwrap();
370        let root_dtype =
371            partition_root_dtype(&partitioned.partition_names, &partitioned.partitions);
372        assert_eq!(
373            partitioned.root,
374            get_item("a_0", get_item("a", root()))
375                .bind(&root_dtype)
376                .unwrap()
377        );
378    }
379
380    #[rstest]
381    fn test_expr_top_level_ref_get_item_and_split_pack(dtype: DType) {
382        let expr = pack(
383            [
384                ("x", get_item("x", get_item("a", root()))),
385                ("y", get_item("y", get_item("a", root()))),
386                ("c", get_item("c", root())),
387            ],
388            NonNullable,
389        );
390        let partitioned = partition_by_field(expr.bind(&dtype).unwrap(), &dtype).unwrap();
391
392        let split_a = partitioned.find_partition(&"a".into()).unwrap();
393        assert_eq!(
394            split_a,
395            &pack(
396                [
397                    ("a_0", get_item("x", get_item("a", root()))),
398                    ("a_1", get_item("y", get_item("a", root())))
399                ],
400                NonNullable
401            )
402            .bind(&dtype)
403            .unwrap()
404        );
405    }
406
407    #[rstest]
408    fn test_expr_top_level_ref_get_item_add(dtype: DType) {
409        let expr = checked_add(get_item("y", get_item("a", root())), lit(1));
410        let partitioned = partition_by_field(expr.bind(&dtype).unwrap(), &dtype).unwrap();
411
412        // Whole expr is a single split
413        assert_eq!(partitioned.partitions.len(), 1);
414        let expected = pack([("a_0", expr)], NonNullable).bind(&dtype).unwrap();
415        assert_eq!(partitioned.find_partition(&"a".into()), Some(&expected));
416        let root_dtype =
417            partition_root_dtype(&partitioned.partition_names, &partitioned.partitions);
418        assert_eq!(
419            partitioned.root,
420            get_item("a_0", col("a")).bind(&root_dtype).unwrap()
421        );
422    }
423
424    #[rstest]
425    fn test_expr_top_level_ref_get_item_add_cannot_split(dtype: DType) {
426        let expr = checked_add(get_item("y", get_item("a", root())), get_item("b", root()));
427        let partitioned = partition_by_field(expr.bind(&dtype).unwrap(), &dtype).unwrap();
428
429        // One for id.a and id.b
430        assert_eq!(partitioned.partitions.len(), 2);
431        let expected_a = pack([("a_0", get_item("y", col("a")))], NonNullable)
432            .bind(&dtype)
433            .unwrap();
434        let expected_b = pack([("b_0", col("b"))], NonNullable).bind(&dtype).unwrap();
435        assert_eq!(partitioned.find_partition(&"a".into()), Some(&expected_a));
436        assert_eq!(partitioned.find_partition(&"b".into()), Some(&expected_b));
437        let root_dtype =
438            partition_root_dtype(&partitioned.partition_names, &partitioned.partitions);
439        let expected_root = checked_add(get_item("a_0", col("a")), get_item("b_0", col("b")))
440            .bind(&root_dtype)
441            .unwrap();
442        assert_eq!(partitioned.root, expected_root);
443    }
444
445    #[rstest]
446    fn test_expr_merge(dtype: DType) {
447        let expr = merge([col("a"), pack([("b", col("b"))], NonNullable)]);
448
449        let partitioned = partition_by_field(expr.bind(&dtype).unwrap(), &dtype).unwrap();
450        let expected = merge([get_item("a_0", col("a")), get_item("b_0", col("b"))]);
451        let root_dtype =
452            partition_root_dtype(&partitioned.partition_names, &partitioned.partitions);
453        assert_eq!(
454            partitioned.root,
455            expected.bind(&root_dtype).unwrap(),
456            "{} {}",
457            partitioned.root,
458            expected
459        );
460
461        assert_eq!(partitioned.partitions.len(), 2);
462
463        let part_a = partitioned.find_partition(&"a".into()).unwrap();
464        let expected_a = pack([("a_0", col("a"))], NonNullable);
465        assert_eq!(
466            part_a,
467            &expected_a.bind(&dtype).unwrap(),
468            "{part_a} {expected_a}"
469        );
470
471        let part_b = partitioned.find_partition(&"b".into()).unwrap();
472        let expected_b = pack([("b_0", pack([("b", col("b"))], NonNullable))], NonNullable);
473        assert_eq!(
474            part_b,
475            &expected_b.bind(&dtype).unwrap(),
476            "{part_b} {expected_b}"
477        );
478    }
479
480    #[rstest]
481    fn replacing_partitions_refreshes_root_dtype(dtype: DType) -> VortexResult<()> {
482        let mut partitioned = partition_by_field(col("b").bind(&dtype)?, &dtype)?;
483        let field_dtype = DType::Primitive(I32, Nullable);
484        let replacement = pack([("b_0", root())], NonNullable).bind(&field_dtype)?;
485
486        partitioned.replace_partitions(vec![replacement].into_boxed_slice())?;
487
488        assert_eq!(partitioned.root.dtype(), &field_dtype);
489        Ok(())
490    }
491}