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