This is an automated email from the ASF dual-hosted git repository.
alamb pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-datafusion.git
The following commit(s) were added to refs/heads/master by this push:
new c7f3a70a7 Add additional pruning tests with casts, handle unsupported
predicates better (#3454)
c7f3a70a7 is described below
commit c7f3a70a79ee84070f7e8f770981fc982d8df0a4
Author: Andrew Lamb <[email protected]>
AuthorDate: Tue Sep 20 05:58:57 2022 -0400
Add additional pruning tests with casts, handle unsupported predicates
better (#3454)
* Add tests for pruning, support pruning with constant expressions
* Use downcast_any!
* chore: Remove uneeded use
---
datafusion/core/src/physical_optimizer/pruning.rs | 184 +++++++++++++++++-----
datafusion/core/tests/parquet_pruning.rs | 12 +-
2 files changed, 147 insertions(+), 49 deletions(-)
diff --git a/datafusion/core/src/physical_optimizer/pruning.rs
b/datafusion/core/src/physical_optimizer/pruning.rs
index f5433d7e8..9c53e0a6a 100644
--- a/datafusion/core/src/physical_optimizer/pruning.rs
+++ b/datafusion/core/src/physical_optimizer/pruning.rs
@@ -38,11 +38,13 @@ use crate::{
logical_plan::{Column, DFSchema, Expr, Operator},
physical_plan::{ColumnarValue, PhysicalExpr},
};
+use arrow::record_batch::RecordBatchOptions;
use arrow::{
array::{new_null_array, ArrayRef, BooleanArray},
datatypes::{DataType, Field, Schema, SchemaRef},
record_batch::RecordBatch,
};
+use datafusion_common::{downcast_value, ScalarValue};
use datafusion_expr::expr_rewriter::{ExprRewritable, ExprRewriter};
use datafusion_expr::utils::expr_to_columns;
@@ -169,38 +171,42 @@ impl PruningPredicate {
/// simplified version `b`. The predicates are simplified via the
/// ConstantFolding optimizer pass
pub fn prune<S: PruningStatistics>(&self, statistics: &S) ->
Result<Vec<bool>> {
- // build statistics record batch
- let predicate_array =
- build_statistics_record_batch(statistics, &self.required_columns)
- .and_then(|statistics_batch| {
- // execute predicate expression
- self.predicate_expr.evaluate(&statistics_batch)
- })
- .and_then(|v| match v {
- ColumnarValue::Array(array) => Ok(array),
- ColumnarValue::Scalar(_) => Err(DataFusionError::Internal(
- "predicate expression didn't return an
array".to_string(),
- )),
- })?;
-
- let predicate_array = predicate_array
- .as_any()
- .downcast_ref::<BooleanArray>()
- .ok_or_else(|| {
- DataFusionError::Internal(format!(
- "Expected pruning predicate evaluation to be BooleanArray,
\
- but was {:?}",
- predicate_array
- ))
- })?;
-
- // when the result of the predicate expression for a row group is null
/ undefined,
- // e.g. due to missing statistics, this row group can't be filtered
out,
- // so replace with true
- Ok(predicate_array
- .into_iter()
- .map(|x| x.unwrap_or(true))
- .collect::<Vec<_>>())
+ // build a RecordBatch that contains the min/max values in the
+ // appropriate statistics columns
+ let statistics_batch =
+ build_statistics_record_batch(statistics, &self.required_columns)?;
+
+ // Evaluate the pruning predicate on that record batch.
+ //
+ // Use true when the result of evaluating a predicate
+ // expression on a row group is null (aka `None`). Null can
+ // arise when the statistics are unknown or some calculation
+ // in the predicate means we don't know for sure if the row
+ // group can be filtered out or not. To maintain correctness
+ // the row group must be kept and thus `true` is returned.
+ match self.predicate_expr.evaluate(&statistics_batch)? {
+ ColumnarValue::Array(array) => {
+ let predicate_array = downcast_value!(array, BooleanArray);
+
+ Ok(predicate_array
+ .into_iter()
+ .map(|x| x.unwrap_or(true)) // None -> true per comments
above
+ .collect::<Vec<_>>())
+
+ },
+ // result was a column
+ ColumnarValue::Scalar(ScalarValue::Boolean(v)) => {
+ let v = v.unwrap_or(true); // None -> true per comments above
+ Ok(vec![v; statistics.num_containers()])
+ }
+ other => {
+ Err(DataFusionError::Internal(format!(
+ "Unexpected result of pruning predicate evaluation.
Expected Boolean array \
+ or scalar but got {:?}",
+ other
+ )))
+ }
+ }
}
/// Return a reference to the input schema
@@ -391,8 +397,13 @@ fn build_statistics_record_batch<S: PruningStatistics>(
}
let schema = Arc::new(Schema::new(fields));
- RecordBatch::try_new(schema, arrays)
- .map_err(|err| DataFusionError::Plan(err.to_string()))
+ // provide the count in case there were no needed statistics
+ let mut options = RecordBatchOptions::default();
+ options.row_count = Some(statistics.num_containers());
+
+ RecordBatch::try_new_with_options(schema, arrays, &options).map_err(|err| {
+ DataFusionError::Plan(format!("Can not create statistics record batch:
{}", err))
+ })
}
struct PruningExpressionBuilder<'a> {
@@ -1168,7 +1179,7 @@ mod tests {
}
#[test]
- fn test_build_statistics_no_stats() {
+ fn test_build_statistics_no_required_stats() {
let required_columns = RequiredStatColumns::new();
let statistics = OneContainerStats {
@@ -1177,13 +1188,9 @@ mod tests {
num_containers: 1,
};
- let result =
- build_statistics_record_batch(&statistics,
&required_columns).unwrap_err();
- assert!(
- result.to_string().contains("Invalid argument error"),
- "{}",
- result
- );
+ let batch =
+ build_statistics_record_batch(&statistics,
&required_columns).unwrap();
+ assert_eq!(batch.num_rows(), 1); // had 1 container
}
#[test]
@@ -1858,7 +1865,15 @@ mod tests {
assert_eq!(result, expected_false);
}
- /// Creates setup for int32 chunk pruning
+ /// Creates a setup for chunk pruning, modeling a int32 column "i"
+ /// with 5 different containers (e.g. RowGroups). They have [min,
+ /// max]:
+ ///
+ /// i [-5, 5]
+ /// i [1, 11]
+ /// i [-11, -1]
+ /// i [NULL, NULL]
+ /// i [1, NULL]
fn int32_setup() -> (SchemaRef, TestStatistics) {
let schema = Arc::new(Schema::new(vec![Field::new("i",
DataType::Int32, true)]));
@@ -1922,6 +1937,45 @@ mod tests {
assert_eq!(result, expected_ret);
}
+ #[test]
+ fn prune_int32_col_lte_zero_cast() {
+ let (schema, statistics) = int32_setup();
+
+ // Expression "cast(i as utf8) <= '0'"
+ // i [-5, 5] ==> some rows could pass (must keep)
+ // i [1, 11] ==> no rows can pass in theory, -0.22 (conservatively
keep)
+ // i [-11, -1] ==> no rows could pass in theory (conservatively keep)
+ // i [NULL, NULL] ==> unknown (must keep)
+ // i [1, NULL] ==> no rows can pass (conservatively keep)
+ let expected_ret = vec![true, true, true, true, true];
+
+ // cast(i as utf8) <= 0
+ let expr = cast(col("i"), DataType::Utf8).lt_eq(lit("0"));
+ let p = PruningPredicate::try_new(expr, schema.clone()).unwrap();
+ let result = p.prune(&statistics).unwrap();
+ assert_eq!(result, expected_ret);
+
+ // try_cast(i as utf8) <= 0
+ let expr = try_cast(col("i"), DataType::Utf8).lt_eq(lit("0"));
+ let p = PruningPredicate::try_new(expr, schema.clone()).unwrap();
+ let result = p.prune(&statistics).unwrap();
+ assert_eq!(result, expected_ret);
+
+ // cast(-i as utf8) >= 0
+ let expr =
+ Expr::Negative(Box::new(cast(col("i"),
DataType::Utf8))).gt_eq(lit("0"));
+ let p = PruningPredicate::try_new(expr, schema.clone()).unwrap();
+ let result = p.prune(&statistics).unwrap();
+ assert_eq!(result, expected_ret);
+
+ // try_cast(-i as utf8) >= 0
+ let expr =
+ Expr::Negative(Box::new(try_cast(col("i"),
DataType::Utf8))).gt_eq(lit("0"));
+ let p = PruningPredicate::try_new(expr, schema).unwrap();
+ let result = p.prune(&statistics).unwrap();
+ assert_eq!(result, expected_ret);
+ }
+
#[test]
fn prune_int32_col_eq_zero() {
let (schema, statistics) = int32_setup();
@@ -1941,6 +1995,50 @@ mod tests {
assert_eq!(result, expected_ret);
}
+ #[test]
+ fn prune_int32_col_eq_zero_cast() {
+ let (schema, statistics) = int32_setup();
+
+ // Expression "cast(i as int64) = 0"
+ // i [-5, 5] ==> some rows could pass (must keep)
+ // i [1, 11] ==> no rows can pass (not keep)
+ // i [-11, -1] ==> no rows can pass (not keep)
+ // i [NULL, NULL] ==> unknown (must keep)
+ // i [1, NULL] ==> no rows can pass (not keep)
+ let expected_ret = vec![true, false, false, true, false];
+
+ let expr = cast(col("i"), DataType::Int64).eq(lit(0i64));
+ let p = PruningPredicate::try_new(expr, schema.clone()).unwrap();
+ let result = p.prune(&statistics).unwrap();
+ assert_eq!(result, expected_ret);
+
+ let expr = try_cast(col("i"), DataType::Int64).eq(lit(0i64));
+ let p = PruningPredicate::try_new(expr, schema).unwrap();
+ let result = p.prune(&statistics).unwrap();
+ assert_eq!(result, expected_ret);
+ }
+
+ #[test]
+ fn prune_int32_col_eq_zero_cast_as_str() {
+ let (schema, statistics) = int32_setup();
+
+ // Note the cast is to a string where sorting properties are
+ // not the same as integers
+ //
+ // Expression "cast(i as utf8) = '0'"
+ // i [-5, 5] ==> some rows could pass (keep)
+ // i [1, 11] ==> no rows can pass (could keep)
+ // i [-11, -1] ==> no rows can pass (could keep)
+ // i [NULL, NULL] ==> unknown (keep)
+ // i [1, NULL] ==> no rows can pass (could keep)
+ let expected_ret = vec![true, true, true, true, true];
+
+ let expr = cast(col("i"), DataType::Utf8).eq(lit("0"));
+ let p = PruningPredicate::try_new(expr, schema).unwrap();
+ let result = p.prune(&statistics).unwrap();
+ assert_eq!(result, expected_ret);
+ }
+
#[test]
fn prune_int32_col_lt_neg_one() {
let (schema, statistics) = int32_setup();
diff --git a/datafusion/core/tests/parquet_pruning.rs
b/datafusion/core/tests/parquet_pruning.rs
index 1de1d7c8f..a7b918180 100644
--- a/datafusion/core/tests/parquet_pruning.rs
+++ b/datafusion/core/tests/parquet_pruning.rs
@@ -237,7 +237,7 @@ async fn prune_int32_scalar_fun() {
test_prune(
Scenario::Int32,
"SELECT * FROM t where abs(i) = 1",
- Some(4),
+ Some(0),
Some(0),
3,
)
@@ -249,7 +249,7 @@ async fn prune_int32_complex_expr() {
test_prune(
Scenario::Int32,
"SELECT * FROM t where i+1 = 1",
- Some(4),
+ Some(0),
Some(0),
2,
)
@@ -261,7 +261,7 @@ async fn prune_int32_complex_expr_subtract() {
test_prune(
Scenario::Int32,
"SELECT * FROM t where 1-i > 1",
- Some(4),
+ Some(0),
Some(0),
9,
)
@@ -308,7 +308,7 @@ async fn prune_f64_scalar_fun() {
test_prune(
Scenario::Float64,
"SELECT * FROM t where abs(f-1) <= 0.000001",
- Some(4),
+ Some(0),
Some(0),
1,
)
@@ -321,7 +321,7 @@ async fn prune_f64_complex_expr() {
test_prune(
Scenario::Float64,
"SELECT * FROM t where f+1 > 1.1",
- Some(4),
+ Some(0),
Some(0),
9,
)
@@ -334,7 +334,7 @@ async fn prune_f64_complex_expr_subtract() {
test_prune(
Scenario::Float64,
"SELECT * FROM t where 1-f > 1",
- Some(4),
+ Some(0),
Some(0),
9,
)