1use std::collections::HashMap;
29use std::fmt::{Debug, Display, Formatter};
30use std::sync::Arc;
31
32use datafusion_expr::Operator;
33use datafusion_physical_expr::PhysicalExpr;
34use datafusion_physical_expr::expressions::{BinaryExpr, col, lit};
35use datatypes::arrow::datatypes::Schema;
36use datatypes::value::{OrderedF64, OrderedFloat, Value};
37
38use crate::error;
39use crate::error::Result;
40use crate::expr::{Operand, PartitionExpr, RestrictedOp};
41use crate::function::IcebergHash;
42
43const ZERO: OrderedF64 = OrderedFloat(0.0f64);
44pub(crate) const NORMALIZE_STEP: OrderedF64 = OrderedFloat(1.0f64);
45pub(crate) const CHECK_STEP: OrderedF64 = OrderedFloat(0.5f64);
46
47#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub enum PartitionDim {
50 Column(String),
51 IcebergHash(IcebergHash),
52}
53
54impl PartitionDim {
55 fn from_operand(operand: &Operand) -> Option<Self> {
56 match operand {
57 Operand::Column(column) => Some(Self::Column(column.clone())),
58 Operand::IcebergHash(hash) => Some(Self::IcebergHash(hash.clone())),
59 Operand::Value(_) | Operand::Expr(_) => None,
60 }
61 }
62
63 pub fn to_operand(&self) -> Operand {
64 match self {
65 Self::Column(column) => Operand::Column(column.clone()),
66 Self::IcebergHash(hash) => Operand::IcebergHash(hash.clone()),
67 }
68 }
69}
70
71impl Display for PartitionDim {
72 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
73 match self {
74 Self::Column(column) => write!(f, "{column}"),
75 Self::IcebergHash(hash) => write!(f, "{hash}"),
76 }
77 }
78}
79
80#[allow(unused)]
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct AtomicExpr {
84 pub nucleons: Vec<NucleonExpr>,
86 pub source_expr_index: usize,
89}
90
91impl AtomicExpr {
92 pub fn to_physical_expr(
94 &self,
95 schema: &Schema,
96 field_name: impl Fn(&PartitionDim) -> String,
97 ) -> Arc<dyn PhysicalExpr> {
98 let mut exprs = Vec::with_capacity(self.nucleons.len());
99 for nucleon in &self.nucleons {
100 exprs.push(nucleon.to_physical_expr(schema, &field_name(&nucleon.column)));
101 }
102 let result: Arc<dyn PhysicalExpr> = exprs
103 .into_iter()
104 .reduce(|l, r| Arc::new(BinaryExpr::new(l, Operator::And, r)))
105 .unwrap();
106 result
107 }
108}
109
110impl PartialOrd for AtomicExpr {
111 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
112 Some(self.nucleons.cmp(&other.nucleons))
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
120pub struct NucleonExpr {
121 column: PartitionDim,
122 op: GluonOp,
123 value: OrderedF64,
125}
126
127impl NucleonExpr {
128 pub fn to_physical_expr(&self, schema: &Schema, field_name: &str) -> Arc<dyn PhysicalExpr> {
129 Arc::new(BinaryExpr::new(
130 col(field_name, schema).unwrap(),
131 self.op.to_operator(),
132 lit(*self.value.as_ref()),
133 ))
134 }
135
136 pub fn column(&self) -> &PartitionDim {
138 &self.column
139 }
140
141 pub fn value(&self) -> OrderedF64 {
143 self.value
144 }
145
146 pub fn op(&self) -> &GluonOp {
148 &self.op
149 }
150
151 pub fn new(column: PartitionDim, op: GluonOp, value: OrderedF64) -> Self {
152 Self { column, op, value }
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
161pub enum GluonOp {
162 Eq,
163 NotEq,
164 Lt,
165 LtEq,
166 Gt,
167 GtEq,
168}
169
170impl GluonOp {
171 pub fn to_operator(&self) -> Operator {
172 match self {
173 GluonOp::Eq => Operator::Eq,
174 GluonOp::NotEq => Operator::NotEq,
175 GluonOp::Lt => Operator::Lt,
176 GluonOp::LtEq => Operator::LtEq,
177 GluonOp::Gt => Operator::Gt,
178 GluonOp::GtEq => Operator::GtEq,
179 }
180 }
181}
182
183#[allow(unused)]
187pub struct Collider<'a> {
188 source_exprs: &'a [PartitionExpr],
189
190 pub atomic_exprs: Vec<AtomicExpr>,
191 pub normalized_values: HashMap<PartitionDim, Vec<(Value, OrderedF64)>>,
195}
196
197impl<'a> Collider<'a> {
198 pub fn new(source_exprs: &'a [PartitionExpr]) -> Result<Self> {
199 let mut values: HashMap<PartitionDim, Vec<Value>> = HashMap::new();
201 for expr in source_exprs {
202 Self::collect_column_values_from_expr(expr, &mut values)?;
203 }
204
205 let mut normalized_values: HashMap<PartitionDim, HashMap<Value, OrderedF64>> =
207 HashMap::with_capacity(values.len());
208 for (column, mut column_values) in values {
209 column_values.sort_unstable();
210 column_values.dedup(); #[allow(clippy::mutable_key_type)]
214 let mut value_map = HashMap::with_capacity(column_values.len());
215 let mut start_value = ZERO;
216 for value in column_values {
217 value_map.insert(value, start_value);
218 start_value += NORMALIZE_STEP;
219 }
220 normalized_values.insert(column, value_map);
221 }
222
223 let mut atomic_exprs = Vec::with_capacity(source_exprs.len());
225 for (index, expr) in source_exprs.iter().enumerate() {
226 Self::collide_expr(expr, index, &normalized_values, &mut atomic_exprs)?;
227 }
228 for expr in &mut atomic_exprs {
230 expr.nucleons.sort_unstable();
231 }
232
233 let normalized_values = normalized_values
235 .into_iter()
236 .map(|(col, values)| {
237 let mut values = values.into_iter().collect::<Vec<_>>();
238 values.sort_unstable_by_key(|(_, v)| *v);
239 (col, values)
240 })
241 .collect();
242
243 Ok(Self {
244 source_exprs,
245 atomic_exprs,
246 normalized_values,
247 })
248 }
249
250 fn collect_column_values_from_expr(
252 expr: &PartitionExpr,
253 values: &mut HashMap<PartitionDim, Vec<Value>>,
254 ) -> Result<()> {
255 match (&*expr.lhs, &*expr.rhs) {
257 (dim, Operand::Value(val)) | (Operand::Value(val), dim)
258 if let Some(dim) = PartitionDim::from_operand(dim) =>
259 {
260 values.entry(dim).or_default().push(val.clone());
261 Ok(())
262 }
263 (Operand::Expr(left_expr), Operand::Expr(right_expr)) => {
264 Self::collect_column_values_from_expr(left_expr, values)?;
265 Self::collect_column_values_from_expr(right_expr, values)
266 }
267 _ => error::InvalidExprSnafu { expr: expr.clone() }.fail(),
269 }
270 }
271
272 fn collide_expr(
278 expr: &PartitionExpr,
279 index: usize,
280 normalized_values: &HashMap<PartitionDim, HashMap<Value, OrderedF64>>,
281 result: &mut Vec<AtomicExpr>,
282 ) -> Result<()> {
283 match expr.op {
284 RestrictedOp::Or => {
285 match &*expr.lhs {
289 Operand::Expr(left_expr) => {
290 Self::collide_expr(left_expr, index, normalized_values, result)?;
291 }
292 _ => {
293 return error::InvalidExprSnafu { expr: expr.clone() }.fail();
296 }
297 }
298
299 match &*expr.rhs {
301 Operand::Expr(right_expr) => {
302 Self::collide_expr(right_expr, index, normalized_values, result)?;
303 }
304 _ => {
305 return error::InvalidExprSnafu { expr: expr.clone() }.fail();
308 }
309 }
310 }
311 RestrictedOp::And => {
312 let mut nucleons = Vec::new();
314 Self::collect_nucleons_from_expr(expr, &mut nucleons, normalized_values)?;
315
316 result.push(AtomicExpr {
317 nucleons,
318 source_expr_index: index,
319 });
320 }
321 _ => {
322 let mut nucleons = Vec::new();
324 Self::collect_nucleons_from_expr(expr, &mut nucleons, normalized_values)?;
325
326 result.push(AtomicExpr {
327 nucleons,
328 source_expr_index: index,
329 });
330 }
331 }
332 Ok(())
333 }
334
335 fn collect_nucleons_from_expr(
337 expr: &PartitionExpr,
338 nucleons: &mut Vec<NucleonExpr>,
339 normalized_values: &HashMap<PartitionDim, HashMap<Value, OrderedF64>>,
340 ) -> Result<()> {
341 match expr.op {
342 RestrictedOp::And => {
343 Self::collect_nucleons_from_operand(&expr.lhs, nucleons, normalized_values)?;
345 Self::collect_nucleons_from_operand(&expr.rhs, nucleons, normalized_values)?;
346 }
347 _ => {
348 nucleons.push(Self::try_create_nucleon(
350 &expr.lhs,
351 &expr.op,
352 &expr.rhs,
353 normalized_values,
354 )?);
355 }
356 }
357 Ok(())
358 }
359
360 fn collect_nucleons_from_operand(
362 operand: &Operand,
363 nucleons: &mut Vec<NucleonExpr>,
364 normalized_values: &HashMap<PartitionDim, HashMap<Value, OrderedF64>>,
365 ) -> Result<()> {
366 match operand {
367 Operand::Expr(expr) => {
368 Self::collect_nucleons_from_expr(expr, nucleons, normalized_values)
369 }
370 _ => {
371 error::NoExprOperandSnafu {
373 operand: operand.clone(),
374 }
375 .fail()
376 }
377 }
378 }
379
380 fn try_create_nucleon(
382 lhs: &Operand,
383 op: &RestrictedOp,
384 rhs: &Operand,
385 normalized_values: &HashMap<PartitionDim, HashMap<Value, OrderedF64>>,
386 ) -> Result<NucleonExpr> {
387 let gluon_op = match op {
388 RestrictedOp::Eq => GluonOp::Eq,
389 RestrictedOp::NotEq => GluonOp::NotEq,
390 RestrictedOp::Lt => GluonOp::Lt,
391 RestrictedOp::LtEq => GluonOp::LtEq,
392 RestrictedOp::Gt => GluonOp::Gt,
393 RestrictedOp::GtEq => GluonOp::GtEq,
394 RestrictedOp::And | RestrictedOp::Or => {
395 return error::UnexpectedSnafu {
397 err_msg: format!("Conjunction operation {:?} should be handled elsewhere", op),
398 }
399 .fail();
400 }
401 };
402
403 match (lhs, rhs) {
404 (dim, Operand::Value(val)) if let Some(dim) = PartitionDim::from_operand(dim) => {
405 if let Some(column_values) = normalized_values.get(&dim)
406 && let Some(&normalized_val) = column_values.get(val)
407 {
408 return Ok(NucleonExpr {
409 column: dim,
410 op: gluon_op,
411 value: normalized_val,
412 });
413 }
414 }
415 (Operand::Value(val), dim) if let Some(dim) = PartitionDim::from_operand(dim) => {
416 if let Some(column_values) = normalized_values.get(&dim)
417 && let Some(&normalized_val) = column_values.get(val)
418 {
419 let flipped_op = match gluon_op {
421 GluonOp::Lt => GluonOp::Gt,
422 GluonOp::LtEq => GluonOp::GtEq,
423 GluonOp::Gt => GluonOp::Lt,
424 GluonOp::GtEq => GluonOp::LtEq,
425 op => op, };
427 return Ok(NucleonExpr {
428 column: dim,
429 op: flipped_op,
430 value: normalized_val,
431 });
432 }
433 }
434 _ => {}
435 }
436
437 error::InvalidExprSnafu {
439 expr: PartitionExpr::new(lhs.clone(), op.clone(), rhs.clone()),
440 }
441 .fail()
442 }
443}
444
445#[cfg(test)]
446mod test {
447 use super::*;
448 use crate::expr::col;
449
450 #[test]
451 fn test_collider_basic_value_normalization() {
452 let exprs = vec![
454 col("age").eq(Value::UInt32(25)),
456 col("age").eq(Value::UInt32(30)),
457 col("age").eq(Value::UInt32(25)), col("name").eq(Value::String("alice".into())),
460 col("name").eq(Value::String("bob".into())),
461 col("active").eq(Value::Boolean(true)),
463 col("active").eq(Value::Boolean(false)),
464 col("score").eq(Value::Float64(OrderedFloat(95.5))),
466 col("score").eq(Value::Float64(OrderedFloat(87.2))),
467 ];
468
469 let collider = Collider::new(&exprs).expect("Failed to create collider");
470
471 assert_eq!(collider.normalized_values.len(), 4);
473
474 let age_values = &collider.normalized_values[&PartitionDim::Column("age".to_string())];
476 assert_eq!(age_values.len(), 2);
477 assert_eq!(
478 age_values,
479 &[
480 (Value::UInt32(25), OrderedFloat(0.0f64)),
481 (Value::UInt32(30), OrderedFloat(1.0f64))
482 ]
483 );
484
485 let name_values = &collider.normalized_values[&PartitionDim::Column("name".to_string())];
487 assert_eq!(name_values.len(), 2);
488 assert_eq!(
489 name_values,
490 &[
491 (Value::String("alice".into()), OrderedFloat(0.0f64)),
492 (Value::String("bob".into()), OrderedFloat(1.0f64))
493 ]
494 );
495
496 let active_values =
498 &collider.normalized_values[&PartitionDim::Column("active".to_string())];
499 assert_eq!(active_values.len(), 2);
500 assert_eq!(
501 active_values,
502 &[
503 (Value::Boolean(false), OrderedFloat(0.0f64)),
504 (Value::Boolean(true), OrderedFloat(1.0f64))
505 ]
506 );
507
508 let score_values = &collider.normalized_values[&PartitionDim::Column("score".to_string())];
510 assert_eq!(score_values.len(), 2);
511 assert_eq!(
512 score_values,
513 &[
514 (Value::Float64(OrderedFloat(87.2)), OrderedFloat(0.0f64)),
515 (Value::Float64(OrderedFloat(95.5)), OrderedFloat(1.0f64))
516 ]
517 );
518 }
519
520 #[test]
521 fn test_collider_simple_expressions() {
522 let exprs = vec![col("id").eq(Value::UInt32(1))];
524
525 let collider = Collider::new(&exprs).unwrap();
526 assert_eq!(collider.atomic_exprs.len(), 1);
527 assert_eq!(collider.atomic_exprs[0].nucleons.len(), 1);
528 assert_eq!(collider.atomic_exprs[0].source_expr_index, 0);
529
530 let exprs = vec![
532 col("id")
533 .eq(Value::UInt32(1))
534 .and(col("status").eq(Value::String("active".into()))),
535 ];
536
537 let collider = Collider::new(&exprs).unwrap();
538 assert_eq!(collider.atomic_exprs.len(), 1);
539 assert_eq!(collider.atomic_exprs[0].nucleons.len(), 2);
540
541 let expr = PartitionExpr::new(
543 Operand::Expr(col("id").eq(Value::UInt32(1))),
544 RestrictedOp::Or,
545 Operand::Expr(col("id").eq(Value::UInt32(2))),
546 );
547 let exprs = vec![expr];
548
549 let collider = Collider::new(&exprs).unwrap();
550 assert_eq!(collider.atomic_exprs.len(), 2);
551 assert_eq!(collider.atomic_exprs[0].nucleons.len(), 1);
552 assert_eq!(collider.atomic_exprs[1].nucleons.len(), 1);
553 }
554
555 #[test]
556 fn test_collider_complex_nested_expressions() {
557 let branch1 = col("id")
559 .eq(Value::UInt32(1))
560 .and(col("status").eq(Value::String("active".into())));
561 let branch2 = col("id")
562 .eq(Value::UInt32(2))
563 .and(col("status").eq(Value::String("inactive".into())));
564 let branch3 = col("id").eq(Value::UInt32(3));
565
566 let expr = PartitionExpr::new(
567 Operand::Expr(PartitionExpr::new(
568 Operand::Expr(branch1),
569 RestrictedOp::Or,
570 Operand::Expr(branch2),
571 )),
572 RestrictedOp::Or,
573 Operand::Expr(branch3),
574 );
575
576 let exprs = vec![expr];
577 let collider = Collider::new(&exprs).unwrap();
578
579 assert_eq!(collider.atomic_exprs.len(), 3);
580
581 let total_nucleons: usize = collider
582 .atomic_exprs
583 .iter()
584 .map(|ae| ae.nucleons.len())
585 .sum();
586 assert_eq!(total_nucleons, 5);
587 }
588
589 #[test]
590 fn test_collider_deep_nesting() {
591 let expr = col("a")
593 .eq(Value::UInt32(1))
594 .and(col("b").eq(Value::UInt32(2)))
595 .and(col("c").eq(Value::UInt32(3)))
596 .and(col("d").eq(Value::UInt32(4)));
597
598 let exprs = vec![expr];
599 let collider = Collider::new(&exprs).unwrap();
600
601 assert_eq!(collider.atomic_exprs.len(), 1);
602 assert_eq!(collider.atomic_exprs[0].nucleons.len(), 4);
603
604 for nucleon in &collider.atomic_exprs[0].nucleons {
606 assert_eq!(nucleon.op, GluonOp::Eq);
607 }
608 }
609
610 #[test]
611 fn test_collider_multiple_expressions() {
612 let exprs = vec![
614 col("id").eq(Value::UInt32(1)),
615 col("name").eq(Value::String("alice".into())),
616 col("score").gt_eq(Value::Float64(OrderedFloat(90.0))),
617 ];
618
619 let collider = Collider::new(&exprs).unwrap();
620
621 assert_eq!(collider.atomic_exprs.len(), 3);
623
624 for atomic_expr in &collider.atomic_exprs {
626 assert_eq!(atomic_expr.nucleons.len(), 1);
627 }
628
629 let indices: Vec<usize> = collider
631 .atomic_exprs
632 .iter()
633 .map(|ae| ae.source_expr_index)
634 .collect();
635 assert!(indices.contains(&0));
636 assert!(indices.contains(&1));
637 assert!(indices.contains(&2));
638 }
639
640 #[test]
641 fn test_collider_value_column_order() {
642 let expr1 = PartitionExpr::new(
644 Operand::Value(Value::UInt32(10)),
645 RestrictedOp::Lt,
646 Operand::Column("age".to_string()),
647 ); let expr2 = PartitionExpr::new(
650 Operand::Value(Value::UInt32(20)),
651 RestrictedOp::GtEq,
652 Operand::Column("score".to_string()),
653 ); let exprs = vec![expr1, expr2];
656 let collider = Collider::new(&exprs).unwrap();
657
658 assert_eq!(collider.atomic_exprs.len(), 2);
659
660 let operations: Vec<GluonOp> = collider
662 .atomic_exprs
663 .iter()
664 .map(|ae| ae.nucleons[0].op.clone())
665 .collect();
666
667 assert!(operations.contains(&GluonOp::Gt)); assert!(operations.contains(&GluonOp::LtEq)); }
670
671 #[test]
672 fn test_collider_complex_or_with_different_columns() {
673 let branch1 = col("name")
675 .eq(Value::String("alice".into()))
676 .and(col("age").eq(Value::UInt32(25)));
677
678 let branch2 = col("status")
679 .eq(Value::String("active".into()))
680 .and(PartitionExpr::new(
681 Operand::Column("score".to_string()),
682 RestrictedOp::Gt,
683 Operand::Value(Value::Float64(OrderedFloat(90.0))),
684 ));
685
686 let expr = PartitionExpr::new(
687 Operand::Expr(branch1),
688 RestrictedOp::Or,
689 Operand::Expr(branch2),
690 );
691
692 let exprs = vec![expr];
693 let collider = Collider::new(&exprs).expect("Failed to create collider");
694
695 assert_eq!(collider.atomic_exprs.len(), 2);
697
698 for atomic_expr in &collider.atomic_exprs {
700 assert_eq!(atomic_expr.nucleons.len(), 2);
701 }
702
703 assert_eq!(collider.normalized_values.len(), 4);
705 assert!(
706 collider
707 .normalized_values
708 .contains_key(&PartitionDim::Column("name".to_string()))
709 );
710 assert!(
711 collider
712 .normalized_values
713 .contains_key(&PartitionDim::Column("age".to_string()))
714 );
715 assert!(
716 collider
717 .normalized_values
718 .contains_key(&PartitionDim::Column("status".to_string()))
719 );
720 assert!(
721 collider
722 .normalized_values
723 .contains_key(&PartitionDim::Column("score".to_string()))
724 );
725 }
726
727 #[test]
728 fn test_try_create_nucleon_edge_cases() {
729 let normalized_values = HashMap::new();
730
731 let result = Collider::try_create_nucleon(
733 &col("a"),
734 &RestrictedOp::And,
735 &Operand::Value(Value::UInt32(1)),
736 &normalized_values,
737 );
738 assert!(result.is_err());
739
740 let result = Collider::try_create_nucleon(
742 &col("a"),
743 &RestrictedOp::Or,
744 &Operand::Value(Value::UInt32(1)),
745 &normalized_values,
746 );
747 assert!(result.is_err());
748
749 let result = Collider::try_create_nucleon(
751 &col("a"),
752 &RestrictedOp::Eq,
753 &col("b"),
754 &normalized_values,
755 );
756 assert!(result.is_err());
757
758 let result = Collider::try_create_nucleon(
760 &Operand::Value(Value::UInt32(1)),
761 &RestrictedOp::Eq,
762 &Operand::Value(Value::UInt32(2)),
763 &normalized_values,
764 );
765 assert!(result.is_err());
766
767 let exprs = vec![];
769 let collider = Collider::new(&exprs).unwrap();
770 assert_eq!(collider.atomic_exprs.len(), 0);
771 assert_eq!(collider.normalized_values.len(), 0);
772 }
773}