1use 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
32pub 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 let annotations = descendent_bound_annotations(&expr, annotate_fn);
45 partition_bound_annotations(expr, annotations)
46}
47
48pub 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 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#[derive(Debug)]
99pub struct BoundPartitionedExpr<A> {
100 pub root: BoundExpression,
102 pub partitions: Box<[BoundExpression]>,
104 pub partition_names: FieldNames,
106 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 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 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 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 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 _ => 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 assert_eq!(partitioned.partitions.len(), 0);
344 assert_eq!(partitioned.root, root().bind(&dtype).unwrap());
345
346 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 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 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}