1use std::borrow::Cow;
21use std::sync::Arc;
22
23use datafusion_catalog::Session;
24use datafusion_common::{
25 HashMap, Result, ScalarValue, TableReference, assert_or_internal_err,
26};
27use datafusion_datasource::PartitionedFile;
28use datafusion_datasource::{FileExtensions, ListingTableUrl};
29use datafusion_expr::{BinaryExpr, Operator, lit, utils};
30
31use arrow::{
32 array::AsArray,
33 datatypes::{DataType, Field},
34 record_batch::RecordBatch,
35};
36use datafusion_expr::execution_props::ExecutionProps;
37use datafusion_expr::physical_planning_context::PhysicalPlanningContext;
38use futures::stream::FuturesUnordered;
39use futures::{StreamExt, TryStreamExt, stream::BoxStream};
40use log::{debug, trace};
41
42use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion};
43use datafusion_common::{Column, DFSchema};
44use datafusion_expr::{Expr, Volatility};
45use datafusion_physical_expr::create_physical_expr;
46use object_store::path::Path;
47use object_store::{ObjectMeta, ObjectStore};
48use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, utf8_percent_encode};
49
50const PARTITION_VALUE_ENCODE_SET: &AsciiSet =
51 &CONTROLS.add(b' ').add(b'%').add(b'/').add(b'?').add(b'#');
52
53pub fn expr_applicable_for_cols(col_names: &[&str], expr: &Expr) -> bool {
59 let mut is_applicable = true;
60 expr.apply(|expr| match expr {
61 Expr::Column(Column { name, .. }) => {
62 is_applicable &= col_names.contains(&name.as_str());
63 if is_applicable {
64 Ok(TreeNodeRecursion::Jump)
65 } else {
66 Ok(TreeNodeRecursion::Stop)
67 }
68 }
69 Expr::Literal(_, _)
70 | Expr::Alias(_)
71 | Expr::OuterReferenceColumn(_, _)
72 | Expr::ScalarVariable(_, _)
73 | Expr::Not(_)
74 | Expr::IsNotNull(_)
75 | Expr::IsNull(_)
76 | Expr::IsTrue(_)
77 | Expr::IsFalse(_)
78 | Expr::IsUnknown(_)
79 | Expr::IsNotTrue(_)
80 | Expr::IsNotFalse(_)
81 | Expr::IsNotUnknown(_)
82 | Expr::Negative(_)
83 | Expr::Cast(_)
84 | Expr::TryCast(_)
85 | Expr::BinaryExpr(_)
86 | Expr::Between(_)
87 | Expr::Like(_)
88 | Expr::SimilarTo(_)
89 | Expr::InList(_)
90 | Expr::Exists(_)
91 | Expr::InSubquery(_)
92 | Expr::ScalarSubquery(_)
93 | Expr::SetComparison(_)
94 | Expr::GroupingSet(_)
95 | Expr::Case(_)
96 | Expr::Lambda(_)
97 | Expr::LambdaVariable(_) => Ok(TreeNodeRecursion::Continue),
98
99 Expr::ScalarFunction(scalar_function) => {
100 match scalar_function.func.signature().volatility {
101 Volatility::Immutable => Ok(TreeNodeRecursion::Continue),
102 Volatility::Stable | Volatility::Volatile => {
105 is_applicable = false;
106 Ok(TreeNodeRecursion::Stop)
107 }
108 }
109 }
110 Expr::HigherOrderFunction(hof) => {
111 match hof.func.signature().volatility {
112 Volatility::Immutable => Ok(TreeNodeRecursion::Continue),
113 Volatility::Stable | Volatility::Volatile => {
116 is_applicable = false;
117 Ok(TreeNodeRecursion::Stop)
118 }
119 }
120 }
121
122 #[expect(deprecated)]
129 Expr::AggregateFunction { .. }
130 | Expr::WindowFunction { .. }
131 | Expr::Wildcard { .. }
132 | Expr::Unnest { .. }
133 | Expr::Placeholder(_) => {
134 is_applicable = false;
135 Ok(TreeNodeRecursion::Stop)
136 }
137 })
138 .unwrap();
139 is_applicable
140}
141
142const CONCURRENCY_LIMIT: usize = 100;
144
145#[derive(Debug)]
146pub struct Partition {
147 path: Path,
149 depth: usize,
152 files: Option<Vec<ObjectMeta>>,
154}
155
156impl Partition {
157 async fn list(mut self, store: &dyn ObjectStore) -> Result<(Self, Vec<Path>)> {
160 trace!("Listing partition {}", self.path);
161 let prefix = Some(&self.path).filter(|p| !p.as_ref().is_empty());
162 let result = store.list_with_delimiter(prefix).await?;
163 self.files = Some(
164 result
165 .objects
166 .into_iter()
167 .filter(|object_meta| object_meta.size > 0)
168 .collect(),
169 );
170 Ok((self, result.common_prefixes))
171 }
172}
173
174pub async fn list_partitions(
176 store: &dyn ObjectStore,
177 table_path: &ListingTableUrl,
178 max_depth: usize,
179 partition_prefix: Option<Path>,
180) -> Result<Vec<Partition>> {
181 let partition = Partition {
182 path: match partition_prefix {
183 Some(prefix) => Path::from_iter(
184 Path::from(table_path.prefix().as_ref())
185 .parts()
186 .chain(Path::from(prefix.as_ref()).parts()),
187 ),
188 None => table_path.prefix().clone(),
189 },
190 depth: 0,
191 files: None,
192 };
193
194 let mut out = Vec::with_capacity(64);
195
196 let mut pending = vec![];
197 let mut futures = FuturesUnordered::new();
198 futures.push(partition.list(store));
199
200 while let Some((partition, paths)) = futures.next().await.transpose()? {
201 if let Some(next) = pending.pop() {
205 futures.push(next)
206 }
207
208 let depth = partition.depth;
209 out.push(partition);
210 for path in paths {
211 let child = Partition {
212 path,
213 depth: depth + 1,
214 files: None,
215 };
216 match depth < max_depth {
217 true => match futures.len() < CONCURRENCY_LIMIT {
218 true => futures.push(child.list(store)),
219 false => pending.push(child.list(store)),
220 },
221 false => out.push(child),
222 }
223 }
224 }
225 Ok(out)
226}
227
228#[derive(Debug)]
229enum PartitionValue {
230 Single(String),
231 Multi,
232}
233
234fn populate_partition_values<'a>(
235 partition_values: &mut HashMap<&'a str, PartitionValue>,
236 filter: &'a Expr,
237) {
238 if let Expr::BinaryExpr(BinaryExpr { left, op, right }) = filter {
239 match op {
240 Operator::Eq => match (left.as_ref(), right.as_ref()) {
241 (Expr::Column(Column { name, .. }), Expr::Literal(val, _))
242 | (Expr::Literal(val, _), Expr::Column(Column { name, .. }))
243 if partition_values
244 .insert(name, PartitionValue::Single(val.to_string()))
245 .is_some() =>
246 {
247 partition_values.insert(name, PartitionValue::Multi);
248 }
249 (Expr::Column(Column { .. }), Expr::Literal(_, _))
250 | (Expr::Literal(_, _), Expr::Column(Column { .. })) => {}
251 _ => {}
252 },
253 Operator::And => {
254 populate_partition_values(partition_values, left);
255 populate_partition_values(partition_values, right);
256 }
257 _ => {}
258 }
259 }
260}
261
262pub fn evaluate_partition_prefix<'a>(
263 partition_cols: &'a [(String, DataType)],
264 filters: &'a [Expr],
265) -> Option<Path> {
266 let mut partition_values = HashMap::new();
267 for filter in filters {
268 populate_partition_values(&mut partition_values, filter);
269 }
270
271 if partition_values.is_empty() {
272 return None;
273 }
274
275 let mut parts = vec![];
276 for (p, _) in partition_cols {
277 match partition_values.get(p.as_str()) {
278 Some(PartitionValue::Single(val)) => {
279 let encoded = encode_partition_value(val);
282 if encoded != val.as_str() {
283 break;
289 }
290 parts.push(format!("{p}={encoded}"));
291 }
292 _ => {
293 break;
296 }
297 }
298 }
299
300 if parts.is_empty() {
301 None
302 } else {
303 Some(Path::from_iter(parts))
304 }
305}
306
307fn encode_partition_value(value: &str) -> Cow<'_, str> {
308 utf8_percent_encode(value, PARTITION_VALUE_ENCODE_SET).into()
309}
310
311pub fn filter_partitioned_file(
312 pf: PartitionedFile,
313 filters: &[Expr],
314 df_schema: &DFSchema,
315) -> Result<Option<PartitionedFile>> {
316 if pf.partition_values.is_empty() && !filters.is_empty() {
317 return Ok(None);
318 } else if filters.is_empty() {
319 return Ok(Some(pf));
320 }
321
322 let arrays = pf
323 .partition_values
324 .iter()
325 .map(|v| v.to_array())
326 .collect::<Result<_, _>>()?;
327
328 let batch = RecordBatch::try_new(Arc::clone(df_schema.inner()), arrays)?;
329
330 let filter = utils::conjunction(filters.iter().cloned()).unwrap_or_else(|| lit(true));
331 let props = ExecutionProps::new();
332 let expr = create_physical_expr(
333 &filter,
334 df_schema,
335 &props,
336 &PhysicalPlanningContext::default(),
337 )?;
338
339 let matches = expr.evaluate(&batch)?.into_array(1)?;
342 if matches.as_boolean().value(0) {
343 return Ok(Some(pf));
344 }
345
346 Ok(None)
347}
348
349fn try_into_partitioned_file(
354 object_meta: ObjectMeta,
355 partition_cols: &[(String, DataType)],
356 table_path: &ListingTableUrl,
357) -> Result<Option<PartitionedFile>> {
358 let cols = partition_cols.iter().map(|(name, _)| name.as_str());
359 let parsed = parse_partitions_for_path(table_path, &object_meta.location, cols);
360
361 let Some(parsed) = parsed else {
362 return Ok(None);
364 };
365
366 let partition_values = parsed
367 .into_iter()
368 .zip(partition_cols)
369 .map(|(parsed, (_, datatype))| {
370 ScalarValue::try_from_string(parsed.into_owned(), datatype)
371 })
372 .collect::<Result<Vec<_>>>()?;
373
374 let mut pf: PartitionedFile = object_meta.into();
375 pf.partition_values = partition_values;
376 pf.table_reference.clone_from(table_path.get_table_ref());
377
378 Ok(Some(pf))
379}
380
381pub async fn pruned_partition_list<'a>(
386 ctx: &'a dyn Session,
387 store: &'a dyn ObjectStore,
388 table_path: &'a ListingTableUrl,
389 filters: &'a [Expr],
390 file_extension: &'a str,
391 partition_cols: &'a [(String, DataType)],
392) -> Result<BoxStream<'a, Result<PartitionedFile>>> {
393 let prefix = if !partition_cols.is_empty() {
394 evaluate_partition_prefix(partition_cols, filters)
395 } else {
396 None
397 };
398
399 let objects = table_path
400 .list_prefixed_files(ctx, store, prefix, file_extension)
401 .await?
402 .try_filter(|object_meta| futures::future::ready(object_meta.size > 0));
403
404 if partition_cols.is_empty() {
405 assert_or_internal_err!(
406 filters.is_empty(),
407 "Got partition filters for unpartitioned table {}",
408 table_path
409 );
410
411 Ok(objects
413 .try_filter_map(|object_meta| {
414 futures::future::ready(object_meta_to_partitioned_file(
415 object_meta,
416 table_path.get_table_ref(),
417 ))
418 })
419 .boxed())
420 } else {
421 let df_schema = DFSchema::from_unqualified_fields(
422 partition_cols
423 .iter()
424 .map(|(n, d)| Field::new(n, d.clone(), true))
425 .collect(),
426 Default::default(),
427 )?;
428
429 Ok(objects
430 .try_filter_map(|object_meta| {
431 futures::future::ready(try_into_partitioned_file(
432 object_meta,
433 partition_cols,
434 table_path,
435 ))
436 })
437 .try_filter_map(move |pf| {
438 futures::future::ready(filter_partitioned_file(pf, filters, &df_schema))
439 })
440 .boxed())
441 }
442}
443
444fn object_meta_to_partitioned_file(
445 object_meta: ObjectMeta,
446 table_ref: &Option<TableReference>,
447) -> Result<Option<PartitionedFile>> {
448 Ok(Some(PartitionedFile {
449 object_meta,
450 arrow_schema: None,
451 partition_values: vec![],
452 range: None,
453 statistics: None,
454 ordering: None,
455 extensions: FileExtensions::new(),
456 metadata_size_hint: None,
457 table_reference: table_ref.clone(),
458 }))
459}
460
461pub fn parse_partitions_for_path<'a, I>(
467 table_path: &ListingTableUrl,
468 file_path: &'a Path,
469 table_partition_cols: I,
470) -> Option<Vec<Cow<'a, str>>>
471where
472 I: IntoIterator<Item = &'a str>,
473{
474 let subpath = table_path.strip_prefix(file_path)?;
475
476 let mut part_values = vec![];
477 for (part, expected_partition) in subpath.zip(table_partition_cols) {
478 match part.split_once('=') {
479 Some((name, val)) if name == expected_partition => {
480 let decoded = percent_decode_str(val)
482 .decode_utf8()
483 .unwrap_or(Cow::Borrowed(val));
484 part_values.push(decoded);
485 }
486 _ => {
487 debug!(
488 "Ignoring file: file_path='{file_path}', table_path='{table_path}', part='{part}', partition_col='{expected_partition}'",
489 );
490 return None;
491 }
492 }
493 }
494 Some(part_values)
495}
496pub fn describe_partition(partition: &Partition) -> (&str, usize, Vec<&str>) {
498 (
499 partition.path.as_ref(),
500 partition.depth,
501 partition
502 .files
503 .as_ref()
504 .map(|f| f.iter().map(|f| f.location.filename().unwrap()).collect())
505 .unwrap_or_default(),
506 )
507}
508
509#[cfg(test)]
510mod tests {
511 use datafusion_datasource::file_groups::FileGroup;
512 use std::ops::Not;
513
514 use super::*;
515 use datafusion_expr::{case, col};
516
517 #[test]
518 fn test_split_files() {
519 let new_partitioned_file = |path: &str| PartitionedFile::new(path.to_owned(), 10);
520 let files = FileGroup::new(vec![
521 new_partitioned_file("a"),
522 new_partitioned_file("b"),
523 new_partitioned_file("c"),
524 new_partitioned_file("d"),
525 new_partitioned_file("e"),
526 ]);
527
528 let chunks = files.clone().split_files(1);
529 assert_eq!(1, chunks.len());
530 assert_eq!(5, chunks[0].len());
531
532 let chunks = files.clone().split_files(2);
533 assert_eq!(2, chunks.len());
534 assert_eq!(3, chunks[0].len());
535 assert_eq!(2, chunks[1].len());
536
537 let chunks = files.clone().split_files(5);
538 assert_eq!(5, chunks.len());
539 assert_eq!(1, chunks[0].len());
540 assert_eq!(1, chunks[1].len());
541 assert_eq!(1, chunks[2].len());
542 assert_eq!(1, chunks[3].len());
543 assert_eq!(1, chunks[4].len());
544
545 let chunks = files.clone().split_files(123);
546 assert_eq!(5, chunks.len());
547 assert_eq!(1, chunks[0].len());
548 assert_eq!(1, chunks[1].len());
549 assert_eq!(1, chunks[2].len());
550 assert_eq!(1, chunks[3].len());
551 assert_eq!(1, chunks[4].len());
552
553 let empty_group = FileGroup::default();
554 let chunks = empty_group.split_files(2);
555 assert_eq!(0, chunks.len());
556 }
557
558 #[test]
559 fn test_parse_partitions_for_path() {
560 assert_eq!(
561 Some(vec![] as Vec<Cow<'_, str>>),
562 parse_partitions_for_path(
563 &ListingTableUrl::parse("file:///bucket/mytable").unwrap(),
564 &Path::from("bucket/mytable/file.csv"),
565 vec![]
566 )
567 );
568 assert_eq!(
569 None,
570 parse_partitions_for_path(
571 &ListingTableUrl::parse("file:///bucket/othertable").unwrap(),
572 &Path::from("bucket/mytable/file.csv"),
573 vec![]
574 )
575 );
576 assert_eq!(
577 None,
578 parse_partitions_for_path(
579 &ListingTableUrl::parse("file:///bucket/mytable").unwrap(),
580 &Path::from("bucket/mytable/file.csv"),
581 vec!["mypartition"]
582 )
583 );
584 assert_eq!(
585 Some(vec![Cow::Borrowed("v1")]),
586 parse_partitions_for_path(
587 &ListingTableUrl::parse("file:///bucket/mytable").unwrap(),
588 &Path::from("bucket/mytable/mypartition=v1/file.csv"),
589 vec!["mypartition"]
590 )
591 );
592 for (path, column, expected) in [
593 (
594 "bucket/mytable/mypartition=v%2F1/file.csv",
595 "mypartition",
596 "v/1",
597 ),
598 (
599 "bucket/mytable/name=John%20Doe/file.csv",
600 "name",
601 "John Doe",
602 ),
603 (
604 "bucket/mytable/mypartition=test%20dir%2Ffile/file.csv",
605 "mypartition",
606 "test dir/file",
607 ),
608 (
609 "bucket/mytable/mypartition=%C3%A9/file.csv",
610 "mypartition",
611 "é",
612 ),
613 (
614 "bucket/mytable/mypartition=%FF/file.csv",
615 "mypartition",
616 "%FF",
617 ),
618 ] {
619 assert_eq!(
620 Some(vec![Cow::Borrowed(expected)]),
621 parse_partitions_for_path(
622 &ListingTableUrl::parse("file:///bucket/mytable").unwrap(),
623 &Path::parse(path).unwrap(),
624 vec![column]
625 )
626 );
627 }
628 assert_eq!(
629 Some(vec![Cow::Borrowed("v1")]),
630 parse_partitions_for_path(
631 &ListingTableUrl::parse("file:///bucket/mytable/").unwrap(),
632 &Path::from("bucket/mytable/mypartition=v1/file.csv"),
633 vec!["mypartition"]
634 )
635 );
636 assert_eq!(
638 None,
639 parse_partitions_for_path(
640 &ListingTableUrl::parse("file:///bucket/mytable").unwrap(),
641 &Path::from("bucket/mytable/v1/file.csv"),
642 vec!["mypartition"]
643 )
644 );
645 assert_eq!(
646 Some(vec![Cow::Borrowed("v1"), Cow::Borrowed("v2")]),
647 parse_partitions_for_path(
648 &ListingTableUrl::parse("file:///bucket/mytable").unwrap(),
649 &Path::from("bucket/mytable/mypartition=v1/otherpartition=v2/file.csv"),
650 vec!["mypartition", "otherpartition"]
651 )
652 );
653 assert_eq!(
654 Some(vec![Cow::Borrowed("v1")]),
655 parse_partitions_for_path(
656 &ListingTableUrl::parse("file:///bucket/mytable").unwrap(),
657 &Path::from("bucket/mytable/mypartition=v1/otherpartition=v2/file.csv"),
658 vec!["mypartition"]
659 )
660 );
661 }
662
663 #[test]
664 fn test_try_into_partitioned_file_valid_partition() {
665 let table_path = ListingTableUrl::parse("file:///bucket/mytable").unwrap();
666 let partition_cols = vec![("year_month".to_string(), DataType::Utf8)];
667 let meta = ObjectMeta {
668 location: Path::from("bucket/mytable/year_month=2024-01/data.parquet"),
669 last_modified: chrono::Utc::now(),
670 size: 100,
671 e_tag: None,
672 version: None,
673 };
674
675 let result =
676 try_into_partitioned_file(meta, &partition_cols, &table_path).unwrap();
677 assert!(result.is_some());
678 let pf = result.unwrap();
679 assert_eq!(pf.partition_values.len(), 1);
680 assert_eq!(
681 pf.partition_values[0],
682 ScalarValue::Utf8(Some("2024-01".to_string()))
683 );
684 }
685
686 #[test]
687 fn test_try_into_partitioned_file_decodes_partition_value() {
688 let table_path = ListingTableUrl::parse("file:///bucket/mytable").unwrap();
689 let partition_cols = vec![("category".to_string(), DataType::Utf8)];
690 let meta = ObjectMeta {
691 location: Path::parse(
692 "bucket/mytable/category=Electronics%2FComputers/data.parquet",
693 )
694 .unwrap(),
695 last_modified: chrono::Utc::now(),
696 size: 100,
697 e_tag: None,
698 version: None,
699 };
700
701 let result =
702 try_into_partitioned_file(meta, &partition_cols, &table_path).unwrap();
703 assert!(result.is_some());
704 let pf = result.unwrap();
705 assert_eq!(pf.partition_values.len(), 1);
706 assert_eq!(
707 pf.partition_values[0],
708 ScalarValue::Utf8(Some("Electronics/Computers".to_string()))
709 );
710 }
711
712 #[test]
713 fn test_try_into_partitioned_file_root_file_skipped() {
714 let table_path = ListingTableUrl::parse("file:///bucket/mytable").unwrap();
718 let partition_cols = vec![("year_month".to_string(), DataType::Utf8)];
719 let meta = ObjectMeta {
720 location: Path::from("bucket/mytable/data.parquet"),
721 last_modified: chrono::Utc::now(),
722 size: 100,
723 e_tag: None,
724 version: None,
725 };
726
727 let result =
728 try_into_partitioned_file(meta, &partition_cols, &table_path).unwrap();
729 assert!(
730 result.is_none(),
731 "Files outside partition structure should be skipped"
732 );
733 }
734
735 #[test]
736 fn test_try_into_partitioned_file_wrong_partition_name() {
737 let table_path = ListingTableUrl::parse("file:///bucket/mytable").unwrap();
739 let partition_cols = vec![("year_month".to_string(), DataType::Utf8)];
740 let meta = ObjectMeta {
741 location: Path::from("bucket/mytable/wrong_col=2024-01/data.parquet"),
742 last_modified: chrono::Utc::now(),
743 size: 100,
744 e_tag: None,
745 version: None,
746 };
747
748 let result =
749 try_into_partitioned_file(meta, &partition_cols, &table_path).unwrap();
750 assert!(
751 result.is_none(),
752 "Files with wrong partition column name should be skipped"
753 );
754 }
755
756 #[test]
757 fn test_try_into_partitioned_file_multiple_partitions() {
758 let table_path = ListingTableUrl::parse("file:///bucket/mytable").unwrap();
759 let partition_cols = vec![
760 ("year".to_string(), DataType::Utf8),
761 ("month".to_string(), DataType::Utf8),
762 ];
763 let meta = ObjectMeta {
764 location: Path::from("bucket/mytable/year=2024/month=01/data.parquet"),
765 last_modified: chrono::Utc::now(),
766 size: 100,
767 e_tag: None,
768 version: None,
769 };
770
771 let result =
772 try_into_partitioned_file(meta, &partition_cols, &table_path).unwrap();
773 assert!(result.is_some());
774 let pf = result.unwrap();
775 assert_eq!(pf.partition_values.len(), 2);
776 assert_eq!(
777 pf.partition_values[0],
778 ScalarValue::Utf8(Some("2024".to_string()))
779 );
780 assert_eq!(
781 pf.partition_values[1],
782 ScalarValue::Utf8(Some("01".to_string()))
783 );
784 }
785
786 #[test]
787 fn test_try_into_partitioned_file_partial_partition_skipped() {
788 let table_path = ListingTableUrl::parse("file:///bucket/mytable").unwrap();
790 let partition_cols = vec![
791 ("year".to_string(), DataType::Utf8),
792 ("month".to_string(), DataType::Utf8),
793 ];
794 let meta = ObjectMeta {
795 location: Path::from("bucket/mytable/year=2024/data.parquet"),
796 last_modified: chrono::Utc::now(),
797 size: 100,
798 e_tag: None,
799 version: None,
800 };
801
802 let result =
803 try_into_partitioned_file(meta, &partition_cols, &table_path).unwrap();
804 assert!(
808 result.is_none(),
809 "Files with incomplete partition structure should be skipped"
810 );
811 }
812
813 #[test]
814 fn test_expr_applicable_for_cols() {
815 assert!(expr_applicable_for_cols(
816 &["c1"],
817 &Expr::eq(col("c1"), lit("value"))
818 ));
819 assert!(!expr_applicable_for_cols(
820 &["c1"],
821 &Expr::eq(col("c2"), lit("value"))
822 ));
823 assert!(!expr_applicable_for_cols(
824 &["c1"],
825 &Expr::eq(col("c1"), col("c2"))
826 ));
827 assert!(expr_applicable_for_cols(
828 &["c1", "c2"],
829 &Expr::eq(col("c1"), col("c2"))
830 ));
831 assert!(expr_applicable_for_cols(
832 &["c1", "c2"],
833 &(Expr::eq(col("c1"), col("c2").alias("c2_alias"))).not()
834 ));
835 assert!(expr_applicable_for_cols(
836 &["c1", "c2"],
837 &(case(col("c1"))
838 .when(lit("v1"), lit(true))
839 .otherwise(lit(false))
840 .expect("valid case expr"))
841 ));
842 assert!(expr_applicable_for_cols(&[], &lit(true)));
846 }
847
848 #[test]
849 fn test_evaluate_partition_prefix() {
850 let partitions = &[
851 ("a".to_string(), DataType::Utf8),
852 ("b".to_string(), DataType::Int16),
853 ("c".to_string(), DataType::Boolean),
854 ];
855
856 assert_eq!(
857 evaluate_partition_prefix(partitions, &[col("a").eq(lit("foo"))]),
858 Some(Path::from("a=foo")),
859 );
860
861 assert_eq!(
862 evaluate_partition_prefix(partitions, &[lit("foo").eq(col("a"))]),
863 Some(Path::from("a=foo")),
864 );
865
866 assert_eq!(
867 evaluate_partition_prefix(
868 partitions,
869 &[col("a").eq(lit("Electronics/Computers"))],
870 ),
871 None,
872 );
873
874 assert_eq!(
875 evaluate_partition_prefix(partitions, &[col("a").eq(lit("John Doe"))]),
876 None,
877 );
878
879 assert_eq!(
880 evaluate_partition_prefix(
881 partitions,
882 &[col("a").eq(lit("foo")).and(col("b").eq(lit("John Doe")))],
883 ),
884 Some(Path::from("a=foo")),
885 );
886
887 assert_eq!(
888 evaluate_partition_prefix(
889 partitions,
890 &[col("a").eq(lit("foo")).and(col("b").eq(lit("bar")))],
891 ),
892 Some(Path::from("a=foo/b=bar")),
893 );
894
895 assert_eq!(
896 evaluate_partition_prefix(
897 partitions,
898 &[col("a").eq(lit("foo")), col("b").eq(lit("bar")),],
900 ),
901 Some(Path::from("a=foo/b=bar")),
902 );
903
904 assert_eq!(
905 evaluate_partition_prefix(
906 partitions,
907 &[col("a")
908 .eq(lit("foo"))
909 .and(col("b").eq(lit("1")))
910 .and(col("c").eq(lit("true")))],
911 ),
912 Some(Path::from("a=foo/b=1/c=true")),
913 );
914
915 assert_eq!(evaluate_partition_prefix(partitions, &[]), None);
917
918 assert_eq!(
920 evaluate_partition_prefix(partitions, &[Expr::eq(col("b"), lit("foo"))]),
921 None,
922 );
923
924 assert_eq!(
926 evaluate_partition_prefix(
927 partitions,
928 &[col("a").eq(lit("foo")).and(col("c").eq(lit("baz")))],
929 ),
930 Some(Path::from("a=foo")),
931 );
932
933 assert_eq!(
935 evaluate_partition_prefix(
936 partitions,
937 &[Expr::and(col("a").eq(lit("foo")), col("a").eq(lit("bar")))],
938 ),
939 None,
940 );
941
942 assert_eq!(
944 evaluate_partition_prefix(
945 partitions,
946 &[Expr::or(col("a").eq(lit("foo")), col("a").eq(lit("bar")))],
947 ),
948 None,
949 );
950 assert_eq!(
951 evaluate_partition_prefix(partitions, &[col("b").lt(lit(5))],),
952 None,
953 );
954 }
955
956 #[test]
957 fn test_evaluate_date_partition_prefix() {
958 let partitions = &[("a".to_string(), DataType::Date32)];
959 assert_eq!(
960 evaluate_partition_prefix(
961 partitions,
962 &[col("a").eq(Expr::Literal(ScalarValue::Date32(Some(3)), None))],
963 ),
964 Some(Path::from("a=1970-01-04")),
965 );
966
967 let partitions = &[("a".to_string(), DataType::Date64)];
968 assert_eq!(
969 evaluate_partition_prefix(
970 partitions,
971 &[col("a").eq(Expr::Literal(
972 ScalarValue::Date64(Some(4 * 24 * 60 * 60 * 1000)),
973 None
974 )),],
975 ),
976 Some(Path::from("a=1970-01-05")),
977 );
978 }
979}