1use std::any::Any;
16use std::cmp::Ordering;
17use std::collections::hash_map::Entry;
18use std::collections::{BTreeSet, HashMap};
19use std::sync::{Arc, RwLock};
20
21use datafusion_common::tree_node::{Transformed, TreeNode};
22use datafusion_expr::{ColumnarValue, Expr};
23use datafusion_physical_expr::PhysicalExpr;
24use datatypes::arrow;
25use datatypes::arrow::array::{Array, BooleanArray, BooleanBufferBuilder, RecordBatch};
26use datatypes::arrow::buffer::BooleanBuffer;
27use datatypes::arrow::compute::prep_null_mask_filter;
28use datatypes::arrow::datatypes::{Field, Schema};
29use datatypes::data_type::DataType as _;
30use datatypes::prelude::Value;
31use datatypes::vectors::{Helper, VectorRef};
32use serde::{Deserialize, Serialize};
33use snafu::{OptionExt, ResultExt, ensure};
34use store_api::storage::RegionNumber;
35
36use crate::PartitionRule;
37use crate::checker::PartitionChecker;
38use crate::error::{self, Result, UndefinedColumnSnafu};
39use crate::expr::{Operand, PartitionExpr, RestrictedOp, plan_physical_expr};
40use crate::function::IcebergHash;
41use crate::partition::RegionMask;
42
43const DEFAULT_REGION: RegionNumber = 0;
45
46type PhysicalExprCache = Option<Arc<PhysicalPartitionExprs>>;
47
48#[derive(Debug)]
50struct PhysicalPartitionExprs {
51 input_schema: Arc<Schema>,
52 exprs: Vec<Arc<dyn PhysicalExpr>>,
53 hash_inputs: Vec<usize>,
57 eval_schema: Arc<Schema>,
58}
59
60#[derive(Debug, Serialize, Deserialize)]
66pub struct MultiDimPartitionRule {
67 partition_columns: Vec<String>,
69 name_to_index: HashMap<String, usize>,
71 regions: Vec<RegionNumber>,
74 exprs: Vec<PartitionExpr>,
76 #[serde(skip)]
78 physical_expr_cache: RwLock<PhysicalExprCache>,
79}
80
81impl MultiDimPartitionRule {
82 pub fn try_new(
87 partition_columns: Vec<String>,
88 regions: Vec<RegionNumber>,
89 exprs: Vec<PartitionExpr>,
90 check_exprs: bool,
91 ) -> Result<Self> {
92 let name_to_index = partition_columns
93 .iter()
94 .enumerate()
95 .map(|(i, name)| (name.clone(), i))
96 .collect::<HashMap<_, _>>();
97
98 let rule = Self {
99 partition_columns,
100 name_to_index,
101 regions,
102 exprs,
103 physical_expr_cache: RwLock::new(None),
104 };
105
106 if check_exprs {
107 let checker = PartitionChecker::try_new(&rule)?;
108 checker.check()?;
109 }
110
111 Ok(rule)
112 }
113
114 pub fn exprs(&self) -> &[PartitionExpr] {
115 &self.exprs
116 }
117
118 fn find_region(&self, values: &[Value]) -> Result<RegionNumber> {
119 ensure!(
120 values.len() == self.partition_columns.len(),
121 error::RegionKeysSizeSnafu {
122 expect: self.partition_columns.len(),
123 actual: values.len(),
124 }
125 );
126
127 for (region_index, expr) in self.exprs.iter().enumerate() {
128 if self.evaluate_expr(expr, values)? {
129 return Ok(self.regions[region_index]);
130 }
131 }
132
133 Ok(DEFAULT_REGION)
135 }
136
137 fn evaluate_expr(&self, expr: &PartitionExpr, values: &[Value]) -> Result<bool> {
138 match (expr.lhs.as_ref(), expr.rhs.as_ref()) {
139 (Operand::Column(name), Operand::Value(r)) => {
140 let index = self
141 .name_to_index
142 .get(name)
143 .context(UndefinedColumnSnafu { column: name })?;
144 let l = &values[*index];
145 Self::perform_op(l, &expr.op, r)
146 }
147 (Operand::Value(l), Operand::Column(name)) => {
148 let index = self
149 .name_to_index
150 .get(name)
151 .context(UndefinedColumnSnafu { column: name })?;
152 let r = &values[*index];
153 Self::perform_op(l, &expr.op, r)
154 }
155 (Operand::IcebergHash(hash), Operand::Value(r)) => {
156 let l = self.evaluate_hash(hash, values)?;
157 Self::perform_op(&l, &expr.op, r)
158 }
159 (Operand::Value(l), Operand::IcebergHash(hash)) => {
160 let r = self.evaluate_hash(hash, values)?;
161 Self::perform_op(l, &expr.op, &r)
162 }
163 (Operand::Expr(lhs), Operand::Expr(rhs)) => {
164 let lhs = self.evaluate_expr(lhs, values)?;
165 let rhs = self.evaluate_expr(rhs, values)?;
166 match expr.op {
167 RestrictedOp::And => Ok(lhs && rhs),
168 RestrictedOp::Or => Ok(lhs || rhs),
169 _ => unreachable!(),
170 }
171 }
172 _ => unreachable!(),
173 }
174 }
175
176 fn evaluate_hash(&self, hash: &IcebergHash, values: &[Value]) -> Result<Value> {
177 let column = hash.column();
178 let index = self
179 .name_to_index
180 .get(column)
181 .context(UndefinedColumnSnafu { column })?;
182 IcebergHash::evaluate_value(&values[*index])
183 }
184
185 fn perform_op(lhs: &Value, op: &RestrictedOp, rhs: &Value) -> Result<bool> {
186 let result = match op {
187 RestrictedOp::Eq => lhs.eq(rhs),
188 RestrictedOp::NotEq => lhs.ne(rhs),
189 RestrictedOp::Lt => lhs.partial_cmp(rhs) == Some(Ordering::Less),
190 RestrictedOp::LtEq => {
191 let result = lhs.partial_cmp(rhs);
192 result == Some(Ordering::Less) || result == Some(Ordering::Equal)
193 }
194 RestrictedOp::Gt => lhs.partial_cmp(rhs) == Some(Ordering::Greater),
195 RestrictedOp::GtEq => {
196 let result = lhs.partial_cmp(rhs);
197 result == Some(Ordering::Greater) || result == Some(Ordering::Equal)
198 }
199 RestrictedOp::And | RestrictedOp::Or => unreachable!(),
200 };
201
202 Ok(result)
203 }
204
205 pub fn row_at(&self, cols: &[VectorRef], index: usize, row: &mut [Value]) -> Result<()> {
206 for (col_idx, col) in cols.iter().enumerate() {
207 row[col_idx] = col.get(index);
208 }
209 Ok(())
210 }
211
212 pub fn record_batch_to_cols(&self, record_batch: &RecordBatch) -> Result<Vec<VectorRef>> {
213 self.partition_columns
214 .iter()
215 .map(|col_name| {
216 record_batch
217 .column_by_name(col_name)
218 .context(UndefinedColumnSnafu { column: col_name })
219 .and_then(|array| {
220 Helper::try_into_vector(array).context(error::ConvertToVectorSnafu)
221 })
222 })
223 .collect::<Result<Vec<_>>>()
224 }
225
226 pub fn split_record_batch_naive(
227 &self,
228 record_batch: &RecordBatch,
229 ) -> Result<HashMap<RegionNumber, BooleanArray>> {
230 let num_rows = record_batch.num_rows();
231
232 let mut result = self
233 .regions
234 .iter()
235 .map(|region| {
236 let mut builder = BooleanBufferBuilder::new(num_rows);
237 builder.append_n(num_rows, false);
238 (*region, builder)
239 })
240 .collect::<HashMap<_, _>>();
241
242 let cols = self.record_batch_to_cols(record_batch)?;
243 let mut current_row = vec![Value::Null; self.partition_columns.len()];
244 for row_idx in 0..num_rows {
245 self.row_at(&cols, row_idx, &mut current_row)?;
246 let current_region = self.find_region(¤t_row)?;
247 let region_mask = result
248 .get_mut(¤t_region)
249 .unwrap_or_else(|| panic!("Region {} must be initialized", current_region));
250 region_mask.set_bit(row_idx, true);
251 }
252
253 Ok(result
254 .into_iter()
255 .map(|(region, mut mask)| (region, BooleanArray::new(mask.finish(), None)))
256 .collect())
257 }
258
259 pub fn split_record_batch(
260 &self,
261 record_batch: &RecordBatch,
262 ) -> Result<HashMap<RegionNumber, RegionMask>> {
263 let num_rows = record_batch.num_rows();
264 if self.regions.len() == 1 {
265 return Ok([(
266 self.regions[0],
267 RegionMask::from(BooleanArray::from(vec![true; num_rows])),
268 )]
269 .into_iter()
270 .collect());
271 }
272 let physical_exprs = {
273 let cache_read_guard = self.physical_expr_cache.read().unwrap();
274 if let Some(cached) = cache_read_guard.as_ref()
275 && &cached.input_schema == record_batch.schema_ref()
276 {
277 cached.clone()
278 } else {
279 drop(cache_read_guard); let new_cache = Arc::new(self.plan_physical_exprs(record_batch.schema())?);
282 let mut cache_write_guard = self.physical_expr_cache.write().unwrap();
283 cache_write_guard.replace(new_cache.clone());
284 new_cache
285 }
286 };
287
288 let eval_batch;
289 let record_batch = if physical_exprs.hash_inputs.is_empty() {
290 record_batch
291 } else {
292 let mut columns = record_batch.columns().to_vec();
293 for index in &physical_exprs.hash_inputs {
294 columns.push(
295 IcebergHash::evaluate_array(record_batch.column(*index).as_ref())
296 .context(error::EvaluateRecordBatchSnafu)?,
297 );
298 }
299 eval_batch = RecordBatch::try_new(physical_exprs.eval_schema.clone(), columns)
300 .context(error::ComputeArrowKernelSnafu)?;
301 &eval_batch
302 };
303
304 let mut result: HashMap<u32, RegionMask> = physical_exprs
305 .exprs
306 .iter()
307 .zip(self.regions.iter())
308 .filter_map(|(expr, region_num)| {
309 let col_val = match expr
310 .evaluate(record_batch)
311 .context(error::EvaluateRecordBatchSnafu)
312 {
313 Ok(array) => array,
314 Err(e) => {
315 return Some(Err(e));
316 }
317 };
318 let array = match columnar_value_to_boolean_array(col_val, num_rows) {
319 Ok(array) if array.null_count() > 0 => prep_null_mask_filter(&array),
322 Ok(array) => array,
323 Err(e) => {
324 return Some(Err(e));
325 }
326 };
327 let selected_rows = array.true_count();
328 if selected_rows == 0 {
329 return None;
331 }
332 Some(Ok((*region_num, RegionMask::new(array, selected_rows))))
333 })
334 .collect::<error::Result<_>>()?;
335
336 let selected = if result.len() == 1 {
337 result.values().next().unwrap().array().clone()
338 } else {
339 let mut selected = BooleanArray::new(BooleanBuffer::new_unset(num_rows), None);
340 for region_mask in result.values() {
341 selected = arrow::compute::kernels::boolean::or(&selected, region_mask.array())
342 .context(error::ComputeArrowKernelSnafu)?;
343 }
344 selected
345 };
346
347 if selected.true_count() == num_rows {
349 return Ok(result);
350 }
351
352 let unselected = arrow::compute::kernels::boolean::not(&selected)
354 .context(error::ComputeArrowKernelSnafu)?;
355 match result.entry(DEFAULT_REGION) {
356 Entry::Occupied(mut o) => {
357 let default_region_mask = RegionMask::from(
359 arrow::compute::kernels::boolean::or(o.get().array(), &unselected)
360 .context(error::ComputeArrowKernelSnafu)?,
361 );
362 o.insert(default_region_mask);
363 }
364 Entry::Vacant(v) => {
365 v.insert(RegionMask::from(unselected));
367 }
368 }
369 Ok(result)
370 }
371
372 fn plan_physical_exprs(&self, input_schema: Arc<Schema>) -> Result<PhysicalPartitionExprs> {
373 let mut hashes = BTreeSet::new();
374 for expr in &self.exprs {
375 collect_hashes(expr, &mut hashes);
376 }
377 if hashes.is_empty() {
378 let exprs = self
379 .exprs
380 .iter()
381 .map(|e| e.try_as_physical_expr(&input_schema))
382 .collect::<Result<Vec<_>>>()?;
383 return Ok(PhysicalPartitionExprs {
384 eval_schema: input_schema.clone(),
385 input_schema,
386 exprs,
387 hash_inputs: vec![],
388 });
389 }
390
391 let mut fields = input_schema.fields().to_vec();
392 let mut hash_columns = HashMap::with_capacity(hashes.len());
393 let mut hash_inputs = Vec::with_capacity(hashes.len());
394 for hash in hashes {
395 let index =
396 input_schema
397 .index_of(hash.column())
398 .ok()
399 .context(UndefinedColumnSnafu {
400 column: hash.column(),
401 })?;
402 let mut name = format!("__partition_hash_{}", hash_inputs.len());
403 while input_schema.field_with_name(&name).is_ok() {
404 name.push('_');
405 }
406 fields.push(Arc::new(Field::new(
407 &name,
408 IcebergHash::output_type().as_arrow_type(),
409 true,
410 )));
411 hash_columns.insert(
412 hash.to_logical_expr(),
413 Expr::Column(datafusion_common::Column::from_name(name)),
414 );
415 hash_inputs.push(index);
416 }
417 let eval_schema = Arc::new(Schema::new(fields));
418
419 let exprs = self
420 .exprs
421 .iter()
422 .map(|expr| {
423 let expr = expr
424 .try_as_logical_expr()?
425 .transform(|expr| {
426 Ok(match hash_columns.get(&expr) {
427 Some(column) => Transformed::yes(column.clone()),
428 None => Transformed::no(expr),
429 })
430 })
431 .context(error::CreatePhysicalExprSnafu)?
432 .data;
433 debug_assert!(
434 !expr
435 .exists(|expr| Ok(matches!(expr, Expr::ScalarFunction(_))))
436 .unwrap(),
437 "iceberg_hash should read the precomputed columns: {expr}"
438 );
439 plan_physical_expr(expr, &eval_schema)
440 })
441 .collect::<Result<Vec<_>>>()?;
442
443 Ok(PhysicalPartitionExprs {
444 input_schema,
445 exprs,
446 hash_inputs,
447 eval_schema,
448 })
449 }
450}
451
452fn collect_hashes(expr: &PartitionExpr, hashes: &mut BTreeSet<IcebergHash>) {
453 for operand in [expr.lhs(), expr.rhs()] {
454 match operand {
455 Operand::IcebergHash(hash) => {
456 hashes.insert(hash.clone());
457 }
458 Operand::Expr(expr) => collect_hashes(expr, hashes),
459 Operand::Column(_) | Operand::Value(_) => {}
460 }
461 }
462}
463
464fn columnar_value_to_boolean_array(
465 col_val: ColumnarValue,
466 num_rows: usize,
467) -> Result<BooleanArray> {
468 let column = col_val
469 .into_array(num_rows)
470 .context(error::EvaluateRecordBatchSnafu)?;
471 let array = column
472 .as_any()
473 .downcast_ref::<BooleanArray>()
474 .with_context(|| error::UnexpectedColumnTypeSnafu {
475 data_type: column.data_type().clone(),
476 })?;
477 Ok(array.clone())
478}
479
480impl PartitionRule for MultiDimPartitionRule {
481 fn as_any(&self) -> &dyn Any {
482 self
483 }
484
485 fn partition_columns(&self) -> &[String] {
486 &self.partition_columns
487 }
488
489 fn find_region(&self, values: &[Value]) -> Result<RegionNumber> {
490 self.find_region(values)
491 }
492
493 fn split_record_batch(
494 &self,
495 record_batch: &RecordBatch,
496 ) -> Result<HashMap<RegionNumber, RegionMask>> {
497 self.split_record_batch(record_batch)
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 use std::assert_matches;
504
505 use super::*;
506 use crate::error::{self, Error};
507 use crate::expr::col;
508
509 #[test]
510 fn test_find_region() {
511 let rule = MultiDimPartitionRule::try_new(
517 vec!["b".to_string()],
518 vec![1, 2, 3],
519 vec![
520 PartitionExpr::new(
521 Operand::Column("b".to_string()),
522 RestrictedOp::Lt,
523 Operand::Value(datatypes::value::Value::String("hz".into())),
524 ),
525 PartitionExpr::new(
526 Operand::Expr(PartitionExpr::new(
527 Operand::Column("b".to_string()),
528 RestrictedOp::GtEq,
529 Operand::Value(datatypes::value::Value::String("hz".into())),
530 )),
531 RestrictedOp::And,
532 Operand::Expr(PartitionExpr::new(
533 Operand::Column("b".to_string()),
534 RestrictedOp::Lt,
535 Operand::Value(datatypes::value::Value::String("sh".into())),
536 )),
537 ),
538 PartitionExpr::new(
539 Operand::Column("b".to_string()),
540 RestrictedOp::GtEq,
541 Operand::Value(datatypes::value::Value::String("sh".into())),
542 ),
543 ],
544 true,
545 )
546 .unwrap();
547 assert_matches!(
548 rule.find_region(&["foo".into(), 1000_i32.into()]),
549 Err(error::Error::RegionKeysSize {
550 expect: 1,
551 actual: 2,
552 ..
553 })
554 );
555 assert_matches!(rule.find_region(&["foo".into()]), Ok(1));
556 assert_matches!(rule.find_region(&["bar".into()]), Ok(1));
557 assert_matches!(rule.find_region(&["hz".into()]), Ok(2));
558 assert_matches!(rule.find_region(&["hzz".into()]), Ok(2));
559 assert_matches!(rule.find_region(&["sh".into()]), Ok(3));
560 assert_matches!(rule.find_region(&["zzzz".into()]), Ok(3));
561 }
562
563 #[test]
564 fn test_find_region_rejects_undeclared_column_on_lhs() {
565 let rule = MultiDimPartitionRule::try_new(
566 vec!["host".to_string()],
567 vec![1],
568 vec![PartitionExpr::new(
569 Operand::Column("rack".to_string()),
570 RestrictedOp::Lt,
571 Operand::Value(Value::String("n".into())),
572 )],
573 false,
574 )
575 .unwrap();
576
577 assert_matches!(
578 rule.find_region(&[Value::String("z".into())]),
579 Err(Error::UndefinedColumn { column, .. }) if column == "rack"
580 );
581 }
582
583 #[test]
584 fn test_find_region_rejects_undeclared_column_on_rhs() {
585 let rule = MultiDimPartitionRule::try_new(
586 vec!["host".to_string()],
587 vec![1],
588 vec![PartitionExpr::new(
589 Operand::Value(Value::String("n".into())),
590 RestrictedOp::Gt,
591 Operand::Column("rack".to_string()),
592 )],
593 false,
594 )
595 .unwrap();
596
597 assert_matches!(
598 rule.find_region(&[Value::String("z".into())]),
599 Err(Error::UndefinedColumn { column, .. }) if column == "rack"
600 );
601 }
602
603 #[test]
604 fn invalid_expr_case_1() {
605 let rule = MultiDimPartitionRule::try_new(
609 vec!["a".to_string(), "b".to_string()],
610 vec![1],
611 vec![PartitionExpr::new(
612 Operand::Column("b".to_string()),
613 RestrictedOp::LtEq,
614 Operand::Expr(PartitionExpr::new(
615 Operand::Expr(PartitionExpr::new(
616 Operand::Column("b".to_string()),
617 RestrictedOp::GtEq,
618 Operand::Value(datatypes::value::Value::String("hz".into())),
619 )),
620 RestrictedOp::And,
621 Operand::Expr(PartitionExpr::new(
622 Operand::Column("b".to_string()),
623 RestrictedOp::Lt,
624 Operand::Value(datatypes::value::Value::String("sh".into())),
625 )),
626 )),
627 )],
628 true,
629 );
630
631 assert_matches!(rule.unwrap_err(), Error::InvalidExpr { .. });
633 }
634
635 #[test]
636 fn invalid_expr_case_2() {
637 let rule = MultiDimPartitionRule::try_new(
641 vec!["a".to_string(), "b".to_string()],
642 vec![1],
643 vec![PartitionExpr::new(
644 Operand::Expr(PartitionExpr::new(
645 Operand::Column("b".to_string()),
646 RestrictedOp::GtEq,
647 Operand::Value(datatypes::value::Value::String("hz".into())),
648 )),
649 RestrictedOp::And,
650 Operand::Value(datatypes::value::Value::String("sh".into())),
651 )],
652 true,
653 );
654
655 assert_matches!(rule.unwrap_err(), Error::InvalidExpr { .. });
657 }
658
659 #[test]
668 fn empty_expr_case_1() {
669 let rule = MultiDimPartitionRule::try_new(
674 vec!["a".to_string(), "b".to_string()],
675 vec![1, 2],
676 vec![
677 PartitionExpr::new(
678 Operand::Column("b".to_string()),
679 RestrictedOp::LtEq,
680 Operand::Value(datatypes::value::Value::String("h".into())),
681 ),
682 PartitionExpr::new(
683 Operand::Column("b".to_string()),
684 RestrictedOp::GtEq,
685 Operand::Value(datatypes::value::Value::String("s".into())),
686 ),
687 ],
688 true,
689 );
690
691 assert_matches!(rule.unwrap_err(), Error::CheckpointNotCovered { .. });
693 }
694
695 #[test]
710 fn empty_expr_case_2() {
711 let rule = MultiDimPartitionRule::try_new(
716 vec!["a".to_string(), "b".to_string()],
717 vec![1, 2],
718 vec![
719 PartitionExpr::new(
720 Operand::Expr(PartitionExpr::new(
721 Operand::Expr(PartitionExpr::new(
722 Operand::Expr(PartitionExpr::new(
724 Operand::Expr(PartitionExpr::new(
725 Operand::Column("a".to_string()),
726 RestrictedOp::GtEq,
727 Operand::Value(datatypes::value::Value::Int64(100)),
728 )),
729 RestrictedOp::And,
730 Operand::Expr(PartitionExpr::new(
731 Operand::Column("b".to_string()),
732 RestrictedOp::LtEq,
733 Operand::Value(datatypes::value::Value::Int64(10)),
734 )),
735 )),
736 RestrictedOp::Or,
737 Operand::Expr(PartitionExpr::new(
739 Operand::Expr(PartitionExpr::new(
740 Operand::Expr(PartitionExpr::new(
741 Operand::Column("a".to_string()),
742 RestrictedOp::Gt,
743 Operand::Value(datatypes::value::Value::Int64(100)),
744 )),
745 RestrictedOp::And,
746 Operand::Expr(PartitionExpr::new(
747 Operand::Column("a".to_string()),
748 RestrictedOp::LtEq,
749 Operand::Value(datatypes::value::Value::Int64(200)),
750 )),
751 )),
752 RestrictedOp::And,
753 Operand::Expr(PartitionExpr::new(
754 Operand::Column("b".to_string()),
755 RestrictedOp::LtEq,
756 Operand::Value(datatypes::value::Value::Int64(10)),
757 )),
758 )),
759 )),
760 RestrictedOp::Or,
761 Operand::Expr(PartitionExpr::new(
763 Operand::Expr(PartitionExpr::new(
764 Operand::Expr(PartitionExpr::new(
765 Operand::Column("a".to_string()),
766 RestrictedOp::GtEq,
767 Operand::Value(datatypes::value::Value::Int64(200)),
768 )),
769 RestrictedOp::And,
770 Operand::Expr(PartitionExpr::new(
771 Operand::Column("b".to_string()),
772 RestrictedOp::Gt,
773 Operand::Value(datatypes::value::Value::Int64(10)),
774 )),
775 )),
776 RestrictedOp::And,
777 Operand::Expr(PartitionExpr::new(
778 Operand::Column("b".to_string()),
779 RestrictedOp::LtEq,
780 Operand::Value(datatypes::value::Value::Int64(20)),
781 )),
782 )),
783 )),
784 RestrictedOp::Or,
785 Operand::Expr(PartitionExpr::new(
787 Operand::Expr(PartitionExpr::new(
788 Operand::Column("a".to_string()),
789 RestrictedOp::Gt,
790 Operand::Value(datatypes::value::Value::Int64(200)),
791 )),
792 RestrictedOp::And,
793 Operand::Expr(PartitionExpr::new(
794 Operand::Column("b".to_string()),
795 RestrictedOp::LtEq,
796 Operand::Value(datatypes::value::Value::Int64(20)),
797 )),
798 )),
799 ),
800 PartitionExpr::new(
801 Operand::Expr(PartitionExpr::new(
803 Operand::Expr(PartitionExpr::new(
804 Operand::Column("a".to_string()),
805 RestrictedOp::Lt,
806 Operand::Value(datatypes::value::Value::Int64(100)),
807 )),
808 RestrictedOp::And,
809 Operand::Expr(PartitionExpr::new(
810 Operand::Column("b".to_string()),
811 RestrictedOp::LtEq,
812 Operand::Value(datatypes::value::Value::Int64(20)),
813 )),
814 )),
815 RestrictedOp::Or,
816 Operand::Expr(PartitionExpr::new(
818 Operand::Expr(PartitionExpr::new(
819 Operand::Column("a".to_string()),
820 RestrictedOp::GtEq,
821 Operand::Value(datatypes::value::Value::Int64(100)),
822 )),
823 RestrictedOp::And,
824 Operand::Expr(PartitionExpr::new(
825 Operand::Column("b".to_string()),
826 RestrictedOp::GtEq,
827 Operand::Value(datatypes::value::Value::Int64(20)),
828 )),
829 )),
830 ),
831 ],
832 true,
833 );
834
835 assert_matches!(rule.unwrap_err(), Error::CheckpointNotCovered { .. });
837 }
838
839 #[test]
840 fn duplicate_expr_case_1() {
841 let rule = MultiDimPartitionRule::try_new(
846 vec!["a".to_string(), "b".to_string()],
847 vec![1, 2],
848 vec![
849 PartitionExpr::new(
850 Operand::Column("a".to_string()),
851 RestrictedOp::LtEq,
852 Operand::Value(datatypes::value::Value::Int64(20)),
853 ),
854 PartitionExpr::new(
855 Operand::Column("a".to_string()),
856 RestrictedOp::GtEq,
857 Operand::Value(datatypes::value::Value::Int64(10)),
858 ),
859 ],
860 true,
861 );
862
863 assert_matches!(rule.unwrap_err(), Error::CheckpointOverlapped { .. });
865 }
866
867 #[test]
868 fn duplicate_expr_case_2() {
869 let rule = MultiDimPartitionRule::try_new(
875 vec!["a".to_string(), "b".to_string()],
876 vec![1, 2],
877 vec![
878 PartitionExpr::new(
879 Operand::Column("a".to_string()),
880 RestrictedOp::NotEq,
881 Operand::Value(datatypes::value::Value::Int64(20)),
882 ),
883 PartitionExpr::new(
884 Operand::Column("a".to_string()),
885 RestrictedOp::LtEq,
886 Operand::Value(datatypes::value::Value::Int64(20)),
887 ),
888 PartitionExpr::new(
889 Operand::Column("a".to_string()),
890 RestrictedOp::Gt,
891 Operand::Value(datatypes::value::Value::Int64(20)),
892 ),
893 ],
894 true,
895 );
896
897 assert_matches!(rule.unwrap_err(), Error::CheckpointOverlapped { .. });
899 }
900
901 #[test]
912 fn test_partial_divided() {
913 let _rule = MultiDimPartitionRule::try_new(
914 vec!["host".to_string(), "value".to_string()],
915 vec![0, 1, 2, 3],
916 vec![
917 col("host")
918 .lt(Value::String("server10".into()))
919 .and(col("value").lt(Value::Int64(10))),
920 col("host")
921 .lt(Value::String("server10".into()))
922 .and(col("value").gt_eq(Value::Int64(10))),
923 col("host").gt_eq(Value::String("server10".into())),
924 ],
925 true,
926 )
927 .unwrap();
928 }
929}
930
931#[cfg(test)]
932mod test_split_record_batch {
933 use std::sync::Arc;
934
935 use common_time::timestamp::Timestamp;
936 use datafusion_common::ScalarValue;
937 use datatypes::arrow::array::{Int64Array, StringArray, TimestampNanosecondArray};
938 use datatypes::arrow::datatypes::{DataType, Field, Schema, TimeUnit};
939 use datatypes::arrow::record_batch::RecordBatch;
940 use rand::Rng;
941
942 use super::*;
943 use crate::expr::{Operand, col};
944
945 fn test_schema() -> Arc<Schema> {
946 Arc::new(Schema::new(vec![
947 Field::new("host", DataType::Utf8, false),
948 Field::new("value", DataType::Int64, false),
949 ]))
950 }
951
952 fn generate_random_record_batch(num_rows: usize) -> RecordBatch {
953 let schema = test_schema();
954 let mut rng = rand::thread_rng();
955 let mut host_array = Vec::with_capacity(num_rows);
956 let mut value_array = Vec::with_capacity(num_rows);
957 for _ in 0..num_rows {
958 host_array.push(format!("server{}", rng.gen_range(0..20)));
959 value_array.push(rng.gen_range(0..20));
960 }
961 let host_array = StringArray::from(host_array);
962 let value_array = Int64Array::from(value_array);
963 RecordBatch::try_new(schema, vec![Arc::new(host_array), Arc::new(value_array)]).unwrap()
964 }
965
966 #[test]
967 fn test_split_record_batch_by_one_column() {
968 let rule = MultiDimPartitionRule::try_new(
970 vec!["host".to_string(), "value".to_string()],
971 vec![0, 1],
972 vec![
973 col("host").lt(Value::String("server1".into())),
974 col("host").gt_eq(Value::String("server1".into())),
975 ],
976 true,
977 )
978 .unwrap();
979
980 let batch = generate_random_record_batch(1000);
981 let result = rule.split_record_batch(&batch).unwrap();
983 let expected = rule.split_record_batch_naive(&batch).unwrap();
984 assert_eq!(result.len(), expected.len());
985 for (region, value) in &result {
986 assert_eq!(
987 value.array(),
988 expected.get(region).unwrap(),
989 "failed on region: {}",
990 region
991 );
992 }
993 }
994
995 #[test]
996 fn test_split_record_batch_widened_time_index_unit_with_hash() {
997 let bound = Value::from(Timestamp::new_millisecond(1_000));
1000 let hash = Operand::IcebergHash(IcebergHash::new("host"));
1001 let exprs = [
1002 (
1003 col("ts").lt(bound.clone()),
1004 hash.clone().lt(Value::Int32(0)),
1005 ),
1006 (
1007 col("ts").lt(bound.clone()),
1008 hash.clone().gt_eq(Value::Int32(0)),
1009 ),
1010 (
1011 col("ts").gt_eq(bound.clone()),
1012 hash.clone().lt(Value::Int32(0)),
1013 ),
1014 (col("ts").gt_eq(bound), hash.gt_eq(Value::Int32(0))),
1015 ]
1016 .into_iter()
1017 .map(|(ts, hash)| ts.and(hash))
1018 .collect();
1019 let rule = MultiDimPartitionRule::try_new(
1020 vec!["ts".to_string(), "host".to_string()],
1021 vec![1, 2, 3, 4],
1022 exprs,
1023 true,
1024 )
1025 .unwrap();
1026
1027 let hosts = ["a", "b", "c", "server42"];
1028 let mut ts = vec![];
1029 let mut host = vec![];
1030 for nanos in [500_000_000, 2_000_000_000] {
1031 for h in hosts {
1032 ts.push(nanos);
1033 host.push(h);
1034 }
1035 }
1036 let schema = Arc::new(Schema::new(vec![
1037 Field::new("ts", DataType::Timestamp(TimeUnit::Nanosecond, None), false),
1038 Field::new("host", DataType::Utf8, false),
1039 ]));
1040 let batch = RecordBatch::try_new(
1041 schema,
1042 vec![
1043 Arc::new(TimestampNanosecondArray::from(ts)),
1044 Arc::new(StringArray::from(host)),
1045 ],
1046 )
1047 .unwrap();
1048
1049 let expected = rule.split_record_batch_naive(&batch).unwrap();
1050 let result = rule.split_record_batch(&batch).unwrap();
1051 for (region, expected) in expected {
1052 let actual = result
1053 .get(®ion)
1054 .map(|mask| mask.array().clone())
1055 .unwrap_or_else(|| BooleanArray::from(vec![false; batch.num_rows()]));
1056 assert_eq!(actual, expected, "region {region}");
1057 }
1058 }
1059
1060 #[test]
1061 fn test_split_record_batch_widened_time_index_unit() {
1062 let bound = Value::from(Timestamp::new_millisecond(1_000));
1067 let rule = MultiDimPartitionRule::try_new(
1068 vec!["ts".to_string()],
1069 vec![0, 1],
1070 vec![col("ts").lt(bound.clone()), col("ts").gt_eq(bound)],
1071 true,
1072 )
1073 .unwrap();
1074
1075 let schema = Arc::new(Schema::new(vec![Field::new(
1076 "ts",
1077 DataType::Timestamp(TimeUnit::Nanosecond, None),
1078 false,
1079 )]));
1080 let batch = RecordBatch::try_new(
1081 schema,
1082 vec![Arc::new(TimestampNanosecondArray::from(vec![
1083 500_000_000,
1084 2_000_000_000,
1085 ]))],
1086 )
1087 .unwrap();
1088
1089 let expected = rule.split_record_batch_naive(&batch).unwrap();
1091 assert_eq!(expected.len(), 2);
1092 let result = rule.split_record_batch(&batch).unwrap();
1093 assert_eq!(result.len(), expected.len());
1094 for (region, value) in &result {
1095 assert_eq!(value.array(), expected.get(region).unwrap());
1096 }
1097 }
1098
1099 #[test]
1100 fn test_split_record_batch_empty() {
1101 let rule = MultiDimPartitionRule::try_new(
1103 vec!["host".to_string()],
1104 vec![1],
1105 vec![
1106 col("host").lt(Value::String("server1".into())),
1107 col("host").gt_eq(Value::String("server1".into())),
1108 ],
1109 true,
1110 )
1111 .unwrap();
1112
1113 let schema = test_schema();
1114 let host_array = StringArray::from(Vec::<&str>::new());
1115 let value_array = Int64Array::from(Vec::<i64>::new());
1116 let batch = RecordBatch::try_new(schema, vec![Arc::new(host_array), Arc::new(value_array)])
1117 .unwrap();
1118
1119 let result = rule.split_record_batch(&batch).unwrap();
1120 assert_eq!(result.len(), 1);
1121 }
1122
1123 #[test]
1124 fn test_split_record_batch_by_two_columns() {
1125 let rule = MultiDimPartitionRule::try_new(
1126 vec!["host".to_string(), "value".to_string()],
1127 vec![0, 1, 2, 3],
1128 vec![
1129 col("host")
1130 .lt(Value::String("server10".into()))
1131 .and(col("value").lt(Value::Int64(10))),
1132 col("host")
1133 .lt(Value::String("server10".into()))
1134 .and(col("value").gt_eq(Value::Int64(10))),
1135 col("host")
1136 .gt_eq(Value::String("server10".into()))
1137 .and(col("value").lt(Value::Int64(10))),
1138 col("host")
1139 .gt_eq(Value::String("server10".into()))
1140 .and(col("value").gt_eq(Value::Int64(10))),
1141 ],
1142 true,
1143 )
1144 .unwrap();
1145
1146 let batch = generate_random_record_batch(1000);
1147 let result = rule.split_record_batch(&batch).unwrap();
1148 let expected = rule.split_record_batch_naive(&batch).unwrap();
1149 assert_eq!(result.len(), expected.len());
1150 for (region, value) in &result {
1151 assert_eq!(value.array(), expected.get(region).unwrap());
1152 }
1153 }
1154
1155 #[test]
1156 fn test_all_rows_selected() {
1157 let rule = MultiDimPartitionRule::try_new(
1159 vec!["value".to_string()],
1160 vec![1, 2],
1161 vec![
1162 col("value").lt(Value::Int64(30)),
1163 col("value").gt_eq(Value::Int64(30)),
1164 ],
1165 true,
1166 )
1167 .unwrap();
1168
1169 let schema = test_schema();
1170 let host_array = StringArray::from(vec!["server1", "server2", "server3", "server4"]);
1171 let value_array = Int64Array::from(vec![10, 20, 30, 40]);
1172 let batch = RecordBatch::try_new(schema, vec![Arc::new(host_array), Arc::new(value_array)])
1173 .unwrap();
1174
1175 let result = rule.split_record_batch(&batch).unwrap();
1176
1177 assert_eq!(result.len(), 2);
1179 assert!(result.contains_key(&1));
1180 assert!(result.contains_key(&2));
1181
1182 assert_eq!(result.get(&1).unwrap().selected_rows(), 2); assert_eq!(result.get(&2).unwrap().selected_rows(), 2); }
1186
1187 #[test]
1188 fn test_split_record_batch_with_scalar_predicate() {
1189 let rule = MultiDimPartitionRule::try_new(
1191 vec!["host".to_string()],
1192 vec![0, 1],
1193 vec![
1194 PartitionExpr::new(
1195 Operand::Column("host".to_string()),
1196 RestrictedOp::Lt,
1197 Operand::Value(Value::String("never_happen_1".into())),
1198 ),
1199 PartitionExpr::new(
1200 Operand::Expr(PartitionExpr::new(
1201 Operand::Column("host".to_string()),
1202 RestrictedOp::GtEq,
1203 Operand::Value(Value::String("never_happen_1".into())),
1204 )),
1205 RestrictedOp::And,
1206 Operand::Value(Value::Boolean(false)),
1207 ),
1208 ],
1209 false,
1210 )
1211 .unwrap();
1212
1213 let batch = generate_random_record_batch(8);
1214 let result = rule.split_record_batch(&batch).unwrap();
1215
1216 assert_eq!(result.len(), 1);
1217 assert!(result.contains_key(&0));
1218
1219 let total_rows = result.get(&0).unwrap().selected_rows();
1220 assert_eq!(total_rows, batch.num_rows());
1221 }
1222
1223 #[test]
1224 fn test_hash_partition_batch_matches_row() {
1225 use datatypes::arrow::array::{Array, StringDictionaryBuilder, UInt64Array};
1226 use datatypes::arrow::datatypes::{DataType, Field, UInt32Type};
1227
1228 let host_hash = Operand::IcebergHash(IcebergHash::new("host"));
1229 let tenant_hash = Operand::IcebergHash(IcebergHash::new("tenant"));
1230 let exprs = [
1231 host_hash
1232 .clone()
1233 .lt(Value::Int32(0))
1234 .and(col("tenant").lt(Value::UInt64(100))),
1235 host_hash
1236 .clone()
1237 .lt(Value::Int32(0))
1238 .and(col("tenant").gt_eq(Value::UInt64(100))),
1239 host_hash
1240 .clone()
1241 .gt_eq(Value::Int32(0))
1242 .and(tenant_hash.clone().lt(Value::Int32(0))),
1243 host_hash
1244 .gt_eq(Value::Int32(0))
1245 .and(tenant_hash.gt_eq(Value::Int32(0))),
1246 ]
1247 .iter()
1248 .map(|expr| {
1249 PartitionExpr::from_json_str(&expr.as_json_str().unwrap())
1250 .unwrap()
1251 .unwrap()
1252 })
1253 .collect();
1254 let rule = MultiDimPartitionRule::try_new(
1255 vec!["host".to_string(), "tenant".to_string()],
1256 vec![1, 2, 3, 4],
1257 exprs,
1258 true,
1259 )
1260 .unwrap();
1261
1262 let hosts = [
1263 None,
1264 Some(""),
1265 Some("a"),
1266 Some("b"),
1267 Some("中🙂"),
1268 Some("server42"),
1269 ];
1270 let tenants = [None, Some(0), Some(99), Some(100), Some(u64::MAX)];
1271 let mut host_builder = StringDictionaryBuilder::<UInt32Type>::new();
1272 let mut tenant_values = vec![];
1273 for host in hosts {
1274 for tenant in tenants {
1275 host_builder.append_option(host);
1276 tenant_values.push(tenant);
1277 }
1278 }
1279 let host_array = Arc::new(host_builder.finish());
1280 let batch = RecordBatch::try_new(
1281 Arc::new(Schema::new(vec![
1282 Field::new("host", host_array.data_type().clone(), true),
1283 Field::new("tenant", DataType::UInt64, true),
1284 ])),
1285 vec![host_array, Arc::new(UInt64Array::from(tenant_values))],
1286 )
1287 .unwrap();
1288
1289 let by_row = rule.split_record_batch_naive(&batch).unwrap();
1290 let by_batch = rule.split_record_batch(&batch).unwrap();
1291 assert!(!by_batch.contains_key(&DEFAULT_REGION));
1292 for (region, expected) in by_row {
1293 let actual = by_batch
1294 .get(®ion)
1295 .map(|mask| mask.array().clone())
1296 .unwrap_or_else(|| BooleanArray::from(vec![false; batch.num_rows()]));
1297 assert_eq!(actual, expected, "region {region}");
1298 }
1299 }
1300
1301 #[test]
1302 fn test_comparison_batch_matches_row() {
1303 use datatypes::arrow::array::Array;
1304 use datatypes::arrow::datatypes::{DataType, Field};
1305
1306 let inputs = [None, Some(4), Some(5), Some(6)];
1307 let batch = RecordBatch::try_new(
1308 Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, true)])),
1309 vec![Arc::new(Int64Array::from(inputs.to_vec()))],
1310 )
1311 .unwrap();
1312 let mut mismatches = vec![];
1313 for op in [
1314 RestrictedOp::Eq,
1315 RestrictedOp::NotEq,
1316 RestrictedOp::Lt,
1317 RestrictedOp::LtEq,
1318 RestrictedOp::Gt,
1319 RestrictedOp::GtEq,
1320 ] {
1321 for bound in [Value::Null, Value::Int64(5)] {
1322 for expr in [
1325 PartitionExpr::new(col("a"), op.clone(), bound.clone().into()),
1326 PartitionExpr {
1327 lhs: Box::new(bound.clone().into()),
1328 op: op.clone(),
1329 rhs: Box::new(col("a")),
1330 },
1331 ] {
1332 let rule = MultiDimPartitionRule::try_new(
1333 vec!["a".to_string()],
1334 vec![1],
1335 vec![expr.clone()],
1336 false,
1337 )
1338 .unwrap();
1339 let by_batch = match expr
1340 .try_as_physical_expr(batch.schema_ref())
1341 .unwrap()
1342 .evaluate(&batch)
1343 {
1344 Ok(by_batch) => {
1345 columnar_value_to_boolean_array(by_batch, batch.num_rows()).unwrap()
1346 }
1347 Err(err) => {
1348 mismatches.push(format!("{expr}: batch error {err}"));
1349 continue;
1350 }
1351 };
1352 for (row, input) in inputs.iter().enumerate() {
1353 let value = input.map_or(Value::Null, Value::Int64);
1354 let by_row = rule.evaluate_expr(&expr, &[value]).unwrap();
1355 let by_batch = by_batch.is_valid(row) && by_batch.value(row);
1356 if by_row != by_batch {
1357 mismatches.push(format!(
1358 "{expr} with a = {input:?}: row {by_row}, batch {by_batch}"
1359 ));
1360 }
1361 }
1362 }
1363 }
1364 }
1365 assert!(mismatches.is_empty(), "{mismatches:#?}");
1366 }
1367
1368 #[test]
1369 fn test_split_null_rows_match_row_routing() {
1370 use datatypes::arrow::datatypes::{DataType, Field};
1371
1372 let rule = MultiDimPartitionRule::try_new(
1373 vec!["host".to_string()],
1374 vec![0, 1],
1375 vec![
1376 col("host").not_eq(Value::from("a")),
1377 col("host").eq(Value::from("a")),
1378 ],
1379 true,
1380 )
1381 .unwrap();
1382 for hosts in [vec![Some("a"), None, Some("z")], vec![Some("a"), None]] {
1383 let batch = RecordBatch::try_new(
1384 Arc::new(Schema::new(vec![Field::new("host", DataType::Utf8, true)])),
1385 vec![Arc::new(StringArray::from(hosts.clone()))],
1386 )
1387 .unwrap();
1388 let by_row = rule.split_record_batch_naive(&batch).unwrap();
1389 let by_batch = rule.split_record_batch(&batch).unwrap();
1390 for (region, expected) in by_row {
1391 let actual = by_batch
1392 .get(®ion)
1393 .map(|mask| mask.array().clone())
1394 .unwrap_or_else(|| BooleanArray::from(vec![false; batch.num_rows()]));
1395 assert_eq!(actual, expected, "{hosts:?}, region {region}");
1396 }
1397 }
1398 }
1399
1400 #[test]
1401 fn test_columnar_value_to_boolean_array_scalar_false() {
1402 let result = columnar_value_to_boolean_array(
1403 ColumnarValue::Scalar(ScalarValue::Boolean(Some(false))),
1404 4,
1405 )
1406 .unwrap();
1407 assert_eq!(result.len(), 4);
1408 assert_eq!(result.true_count(), 0);
1409 }
1410
1411 #[test]
1412 fn test_columnar_value_to_boolean_array_scalar_true() {
1413 let result = columnar_value_to_boolean_array(
1414 ColumnarValue::Scalar(ScalarValue::Boolean(Some(true))),
1415 4,
1416 )
1417 .unwrap();
1418 assert_eq!(result.len(), 4);
1419 assert_eq!(result.true_count(), 4);
1420 }
1421}